Skip to content

Conversation

@krickert
Copy link

@krickert krickert commented Feb 6, 2026

This pull request introduces a professional gRPC client and service bridge for Docling Serve, designed for enterprise-grade pipeline integrations and long-term archival storage.

Key Enhancements

  • Professional gRPC API Design: Implements the ai.docling.serve.v1 contract with strict versioning throughout both Protobuf and Java packages.
  • Unique Request/Response Wrappers: Adheres to Google API Design and Buf linting standards by using dedicated wrapper messages for every RPC, ensuring future-proof method independence.
  • Robust Null-Safety: Implements exhaustive defensive mapping for the entire Docling document schema, guarding all collections, boxed primitives, and enums.
  • Server-Side Polling (Watch Pattern): Introduces "Watch" RPCs that manage asynchronous task monitoring on the server side, streaming status updates back to the client.
  • Comprehensive Verification: Includes unit, bidirectional mapping, and Testcontainers-based integration suites (all 78 tests passing).
  • JPMS Support: Fully compatible with the Java Platform Module System.

Closes #325

@krickert krickert changed the title PR Feature: grpc server issue #325 feat(grpc): implement docling-serve-grpc service with strict versioning Feb 6, 2026
@krickert krickert changed the title feat(grpc): implement docling-serve-grpc service with strict versioning feat(grpc): implement docling-serve-grpc service Feb 6, 2026
@krickert krickert changed the title feat(grpc): implement docling-serve-grpc service feat(client): implement docling-serve-grpc service Feb 6, 2026
@edeandrea
Copy link
Contributor

Thank you @krickert for this! You've definitely put a lot of thought & time into this.

If you don't mind, let me take a few days to go through all of this. I've just returned to the office this morning after being out all week, and I want to give this the thought and review it deserves.

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

:java_duke: JaCoCo coverage report

Overall Project 20.67% 🔴

There is no coverage information present for the Files changed

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

TestsPassed ✅SkippedFailed
Gradle Test Results (all modules & JDKs)1170 ran1170 passed0 skipped0 failed
TestResult
No test annotations available

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

HTML test reports are available as workflow artifacts (zipped HTML).

• Download: Artifacts for this run

@edeandrea
Copy link
Contributor

@all-contributors add @krickert for code,idea

@allcontributors
Copy link
Contributor

@edeandrea

I've put up a pull request to add @krickert! 🎉

@edeandrea
Copy link
Contributor

@all-contributors add @krickert for documentation, tests

@allcontributors
Copy link
Contributor

@edeandrea

I've put up a pull request to add @krickert! 🎉

@edeandrea
Copy link
Contributor

Hi @krickert - when you created the pull request it does not look like you checked the box "Allow edits from maintainers".

Is that something you can turn on? I'd like to add a few things so that the CI will pick up these new modules and run the tests/code coverage on the PR.

@krickert
Copy link
Author

krickert commented Feb 6, 2026

Oh no problem!! Done.

Signed-off-by: Kristian Rickert <krickert@gmail.com>
@edeandrea
Copy link
Contributor

For some reason its still not letting me push on top of your commit. I think its because you created the PR from your main branch rather than a feature branch.

Anyways, can you make the following edits so that CI will run on the PR?

.github/workflows/build.yml

image

test-report-aggregation/build.gradle.kts

image

@krickert
Copy link
Author

krickert commented Feb 6, 2026

Yes. I'll do it within the hour

For some reason its still not letting me push on top of your commit. I think its because you created the PR from your main branch rather than a feature branch.

Anyways, can you make the following edits so that CI will run on the PR?

.github/workflows/build.yml

image

test-report-aggregation/build.gradle.kts

image

Signed-off-by: Kristian Rickert <krickert@gmail.com>
@krickert
Copy link
Author

krickert commented Feb 6, 2026

OK I was able to do it I think?

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

HTML test reports are available as workflow artifacts (zipped HTML).

• Download: Artifacts for this run

Copy link
Contributor

@edeandrea edeandrea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much @krickert for this! You've definitely put a lot of thought & work into this, so its much appreciated.

Sorry for the delay in my review. I wanted to look over it, give it some time to settle in my brain and think about it a bit.

I'd also like @ThomasVitale to look through this as well.

There are a few small general comments I have that I saw in lots of places (note to self - I need to update the contributing guidelines with these so that people are aware):

  1. No wildcard imports please
  2. While we do need to put some linting in place, there is a .editorconfig file in the repository. It would be great if you could configure your IDE to adhere to whats in there.

The biggest part of this that I want to make sure I understand is that DoclingServeGrpcService is a gRPC server that wraps DoclingServeApi. Then the gRPC definitions from the protobuf files can interact with the server?

The thought being that at some point if docling itself supported a gRPC server out of the box, then DoclingServeGrpcService (& the dependency on docling-serve-api) wouldn't even be needed?

If thats the case, should we structure this similar to how we've structured the REST side? One module just for the API (which in the case of gRPC would mostly just be the protobuf definitions)? And then a client module, which would be the wrapper thing thats here?

That way, when/if docling has a grpc server out of the box, the client module would just get deprecated and cease to exist?

@ThomasVitale I'm curious as to your overall thoughts as well.

* @return a {@link TaskStatusPollResponse} containing the task ID and initial status
* @throws ai.docling.serve.api.validation.ValidationException If request validation fails for any reason.
*/
TaskStatusPollResponse submitChunkHybridSource(HybridChunkDocumentRequest request);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as above and on DoclingServeConvertApi

I'm not sure of the purpose for this method? The async variant returns CompletionStage so that the end user doesn't have to do any of the polling/waiting. It's handled directly by the API.

Shouldn't it be the same here? I'm not sure why we want to force the user to have to poll on their own?

@Override
public TaskStatusPollResponse submitChunkHierarchicalSource(HierarchicalChunkDocumentRequest request) {
return this.chunkOps.submitChunkHierarchicalSource(request);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as on DoclingServeConvertApi & DoclingServeChunkApi.

I'm not sure of the purpose for this method? The async variant returns CompletionStage so that the end user doesn't have to do any of the polling/waiting. It's handled directly by the API.

Shouldn't it be the same here? I'm not sure why we want to force the user to have to poll on their own?

* Proto → Java: for incoming gRPC requests that need to call the REST client.
* Java → Proto: for REST client responses that need to be returned as gRPC responses.
*/
public class ServeApiMapper {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Can probably be declared final
  2. I totally understand the purpose of this class, but is there a way we can think of an easy way to maintain it rather than hard-coding everything? Maintainability is something we spent a lot of time thinking about up front. What about using something like Mapstruct? Mapstruct itself wouldn't need to be exposed outside of the module - it generates static code at build time via an annotation processor.

@ThomasVitale thoughts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoclingServeGrpcService. Yup - a bridge/wrapper. I actually have an open ticket with Docling to support gRPC natively (they haven't bit yet), so the goal of this implementation is to serve as a proof-of-concept to motivate them to move in that direction.

To your question about structure: While mirroring the REST setup feels intuitively consistent, I strictly avoid a 1:1 mapping between REST and Protobuf. They play by different rules, and treating them the same introduces long-term risks.

Here is why I want to stick to the standard "v1" Protobuf definition approach:

Best to think of protobufs as Data, not just APIs. Unlike REST, which is ephemeral, Protobuf messages are often used as immutable records for storage (Kafka, logs, cold archival). I plan to use these definitions for long-term storage of Docling outputs. Makes sense too right? It's a document service type output. They even want to standardize this model more, which suggests it'll settle down and not change too much anyway.

If we couple the .proto package structure to our current temporary wrapper structure (to match REST), we bake implementation details into the data.

Therefore --- If Docling later releases a native server and we switch to it, or if we refactor our wrapper, we break backward compatibility with every record we’ve ever stored (and would've had to make definition changes anyway). I want to avoid needing a complex migration/mapping class just to read data we saved a year ago.

I also avoid "REST-in-Protobuf" syntax. When we try to mirror REST 1:1, we usually end up with "bad" Protobuf definitions. We miss out on streaming capabilities, we end up with unnecessary empty request/response objects, or we create definitions that mimic a specific language's (Java/Python) object structure rather than a language-neutral contract.

The API module should contain pure, immutable v1 definitions that look like they came from a native Docling server, agnostic of our specific wrapper.

If/when Docling releases a server, our wrapper module (DoclingServeGrpcService) effectively becomes obsolete and can be deleted. However, because we kept the definitions standard and separate:

  • We won't have to change the data format.
  • The generated clients (stubs) will continue to work with zero coding changes.
  • We avoid the old SOAP-style integration nightmares; the auto-generated stubs just handle the switch.

So if you're ok with this - let’s keep the definitions (the Schema) distinct from the implementation (the Wrapper). The Schema should be designed as if the "perfect" Docling gRPC server already exists and not let the JSON/REST bleed into it. This allows us to use these objects in Kafka, mobile apps, or storage without worrying about the internal structure of our temporary bridge service.

I know it's not as intuitive, but you really don't even need to make separate tags and releases of protobufs if you design it well since the definitions should always line up and a schema registry can automatically track the changes.

import ai.docling.serve.api.task.response.TaskStatus;
import ai.docling.serve.api.task.response.TaskStatusMetadata;
import ai.docling.serve.api.task.response.TaskStatusPollResponse;
import ai.docling.serve.v1.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please no wildcard imports. Please try to format/adhere to the .editorconfig.

If you're using IntelliJ you can configure it to automatically respect it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll setup intellj to lint better..

* Each field is explicitly mapped to maintain strong typing throughout the gRPC stack.
*/
@SuppressWarnings("DataFlowIssue")
public class DoclingDocumentMapper {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty much all the same comments I left in ServeApiMapper

/**
* Shared utilities for proto ↔ Java mapping.
*/
final class ProtoMapping {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a Utils class in docling-serve-api that you could add this to.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do...

…rve-grpc service

Signed-off-by: Kristian Rickert <krickert@gmail.com>
…in `docling-serve-grpc` service

Signed-off-by: Kristian Rickert <krickert@gmail.com>
@krickert
Copy link
Author

Still workin' on the changes - I can get to the rest tonight. Worked out the obvious ones already, looked into MapStruct protobuf plugin, it would be adding another layer though and it's just one API we're mirroring. If the plan is to mirror many, I'd recommend this but the scope of the mapping was 3 classes. I'm going to run this through potentially 100s of millions of docs, so any bugs are going to be found. Finally, it would be fastest this way and any "gotchas" that come out of the REST api can be mapped here. But maintaining the API isn't that bad - it took me less than a day to write up the API.

In return, we get a 1:1 logical mapping with all the protobuf advantages of backward compatibility. Since there's a PR open on the main docling line, let's see where that goes? If the server moves there, the proto client would be available OOTB and you can write a quarkus plugin without this java layer, too.

@edeandrea
Copy link
Contributor

Still workin' on the changes - I can get to the rest tonight. Worked out the obvious ones already, looked into MapStruct protobuf plugin, it would be adding another layer though and it's just one API we're mirroring. If the plan is to mirror many, I'd recommend this but the scope of the mapping was 3 classes. I'm going to run this through potentially 100s of millions of docs, so any bugs are going to be found. Finally, it would be fastest this way and any "gotchas" that come out of the REST api can be mapped here. But maintaining the API isn't that bad - it took me less than a day to write up the API.

I appreciate you looking into things!

Re: the mapping stuff - there is a LOT of logic in the mapping classes, which looks like it is mapping between the protobuf objects and the DoclingServeApi objects. It is "brittle" code that would most likely be dependent on the protobuf version.

@ThomasVitale and I are only 2 people working part time on this project, so at the beginning we said that we really needed to favor maintainability as one of the top concerns.

I wasn't aware there was a mapstruct protobuf thing, I was merely talking about Mapstruct the java mapper. It provides a nice way to map from one object structure to another in a declarative fashion.

Now I'm not saying i want to force it, but just something to look into.

@krickert
Copy link
Author

Still workin' on the changes - I can get to the rest tonight. Worked out the obvious ones already, looked into MapStruct protobuf plugin, it would be adding another layer though and it's just one API we're mirroring. If the plan is to mirror many, I'd recommend this but the scope of the mapping was 3 classes. I'm going to run this through potentially 100s of millions of docs, so any bugs are going to be found. Finally, it would be fastest this way and any "gotchas" that come out of the REST api can be mapped here. But maintaining the API isn't that bad - it took me less than a day to write up the API.

I appreciate you looking into things!

Re: the mapping stuff - there is a LOT of logic in the mapping classes, which looks like it is mapping between the protobuf objects and the DoclingServeApi objects. It is "brittle" code that would most likely be dependent on the protobuf version.

@ThomasVitale and I are only 2 people working part time on this project, so at the beginning we said that we really needed to favor maintainability as one of the top concerns.

I wasn't aware there was a mapstruct protobuf thing, I was merely talking about Mapstruct the java mapper. It provides a nice way to map from one object structure to another in a declarative fashion.

Now I'm not saying i want to force it, but just something to look into.

I think it's worth looking into. I'm looking for a better way too - I use CEL on protobufs on my mainline project to handle my mappings between protobufs... it's faster than MapStruct I'm sure - but would be worth looking into as well. CEL works on both protobufs and pojos, so I think that would be a bit less brittle because it's an industry standard and not just a java library - so I think it can map between the REST Jackson objects to the Protobufs well and even put expressions (like in my case, the default values).

Another suggestion - if you consider it - having the grpc layer be the client instead of a java layer. I think this layer is still needed though, until we get a true grpc client from the server then I'd go that route instead. But even with this, you can just use the grpc client then you don't have to maintain any code just the mapping layer and offer all the functionality of the server on any client... it'll also make it attractive to those who have a grpc environment and would prefer this layer over rest. just a thought :)

@ThomasVitale
Copy link
Contributor

I'll have a look and share my comments later today. Thanks for your patience!

@github-actions
Copy link

HTML test reports are available as workflow artifacts (zipped HTML).

• Download: Artifacts for this run

@ThomasVitale
Copy link
Contributor

Thanks so much @krickert for suggesting this feature and for the work you've been doing. It's much appreciated! And I particularly appreciated all the insightful information and guidelines for gRPC API design that you shared together with the code. As I'm no GRPC expert, that was helpful to me.

I'd like to share some thoughts from different perspectives.

Considering an architectural perspective, I haven't fully grasped the benefits of introducing gRPC if the network calls to the Docling Serve API is done with JSON over HTTP (so the potential bottleneck of JSON parsing and polling remains). If I understand correctly, considering Docling Serve doesn't have a gRPC API, the goal would be more on providing streaming-friendly APIs to the developers using Docling Serve rather than performance. Is that right? If so, wouldn't Quarkus with its convenient reactive core model already offer a nice developer experience via the Mutiny APIs (and its operators to build reactive streams) in the current Quarkus Docling project, using the async Docling Serve APIs?

Considering the maintenance perspective for the Docling Java project, I'm not sure about adding this support in this project at this stage. Here's some of my considerations.

  • I was considering suggesting having at least the protobuf definitions here, generated from the Docling Serve OpenAPI Spec (though, it's really something that should be in the Docling Serve project itself). But if I understood correctly, the PR doesn't rely on a 1-to-1 mapping between the two APIs in order to follow the gRPC best practices in API design (which makes sense), but also to ensure some stability and backward compatibility as v1 (which I also think makes sense). However, the cost for maintaining this different API (or actually, data model, as you mentioned) seems to be pretty high for this project. Both to keep it stable when the official Docling Serve HTTP API evolves, but also simply for making dedicated decisions on how to include new fields, objects, and APIs to the gRPC project in a gRPC-friendly way. The scope of the Docling Java project would change quite a bit, then. Today, we fully rely on the OpenAPI Spec published by Docling Serve. If the gRPC data models are not a 1-to-1 mapping, it means we would also need to design a dedicated contract for that. Potentially, delaying cutting new releases in order to handle that.
  • Continuing the discussion about stability, evolution, and maintenance. The gRPC Server (which is a wrapper around the Docling Serve HTTP API) seems to require quite some work as well. On the one hand, the part about maintaining the contract stable. On the other hand, the part about making it possible to transparently adopt a potential gRPC API in the Docling Serve project if that happens, without changing any client code. Finally, the part about stability, which I'm afraid it's something we cannot guarantee at this stage. The Docling Java project is not GA yet. Breaking changes can and will probably happen until we get to version 1.0, while we refine the design and APIs based on feedback from the community and projects downstream using it (and it's really great you reached out with your feedback and inputs, much appreciated!).
  • The gRPC Server wrapper could potentially be used by itself in a Java app, separate from the app using the gRPC Client. We would need to consider how to make this generic enough to allow downstream integrations to hook it into their gRPC facilities as well. The additional maintenance cost and complexity would propagate to the downstream integrations, I imagine, since they would also need to provide a production-ready server module next to a client module (I'm thinking observability, resilience, and all those things). Also, it might be some users have a gRPC Server wrapper version X running, a gRPC Client running in a separate app with version X. What happens if they update the gRPC Server wrapper to version X+1? Will the gRPC Client version X keep working correctly? These might be theoretical points as I'm not sure someone would run the gRPC Server wrapper in isolation, but if we add it to Docling Java, it's something we should be ready to support or at least be clear about what we support. And we would be moving from a client project to a client and server project, which needs to be taken into account in the way we evolve the project.
  • In order to enable having a gRPC Client in Java to integrate with Docling, in this discussion, we are considering implementing a gRPC Server in Java, wrapping the Java Client API calling the Docling Serve HTTP API. But that wrapper/gRPC Server would make it possible to expose a gRPC API so that any client from any language/framework can consume Docling Serve via gRPC, right? That makes me think that the Docling Java project might not be the best place to add a gRPC Server. I can see that the Docling Serve team hasn't replied to your feature request yet, but I was thinking about whether the gRPC Server implementation should be done in Python, perhaps leading to contributing that to the Docling Serve project directly, if they decide to support gRPC. Or it could be used as an extension to Docling Serve, building a custom Docling Serve artefact that includes upstream Docling Serve + the gRPC bits. That would really bring the benefits for network calls as well.

To sum up, based on all above, I personally don't feel confident introducing this gRPC module into Docling Java. And one of the key points I'd like to mention again is that this project is not GA yet. It's actively evolving and will hopefully reach the first GA version this year. Adding gRPC support now without having official support in the core Docling Serve project doesn't seam a good fit as it will introduce quite some work that would widen the scope of the project and make it harder to get to the first GA release. Things would have been different if Docling Serve had already a gRPC API. But the fact that it doesn't and we need to implement a bridge and design the API/data model for it worries me.

Once again, thank you @krickert for all the work you did! I hope what I shared made sense. If I misunderstood something, please let me know, thank you!

@edeandrea I would also like to hear your thoughts about what I wrote.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate a streaming gRPC client

3 participants