From 5e18cf1f37f3f9053296fa264c26dad1aa57fe85 Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Tue, 1 Sep 2026 13:47:12 -0400 Subject: [PATCH] feat(api): add toBuilder() to DocumentRequest sealed base Lombok's @SuperBuilder(toBuilder = true) only emits toBuilder() on the concrete request subtypes, so it was unreachable through the abstract DocumentRequest (and intermediate ChunkDocumentRequest) supertype. This forced callers to inject a source/target inside a concrete-type dispatch switch. Declare an abstract toBuilder() on DocumentRequest returning the generic DocumentRequest.Builder, which the Lombok-generated concrete toBuilder() methods satisfy as covariant overrides. ChunkDocumentRequest adds a narrowing override returning its own builder. This lets callers clone-and-modify polymorphically: DocumentRequest withSource = request.toBuilder().source(source).build(); Assisted-By: Claude Code Signed-off-by: Eric Deandrea --- .../chunk/request/ChunkDocumentRequest.java | 3 ++ .../serve/api/request/DocumentRequest.java | 15 ++++++++ .../api/request/DocumentRequestTests.java | 38 +++++++++++++++++++ docs/src/doc/docs/whats-new.md | 1 + 4 files changed, 57 insertions(+) diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java index e8d08936..71179328 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java @@ -33,6 +33,9 @@ public sealed abstract class ChunkDocumentRequest extends DocumentRequest @JsonProperty("include_converted_doc") private boolean includeConvertedDoc; + @Override + public abstract ChunkDocumentRequest.Builder toBuilder(); + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") public abstract static class ChunkDocumentRequestBuilder> extends DocumentRequest.DocumentRequestBuilder { } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java index 10e51c6e..54935add 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java @@ -32,6 +32,13 @@ * case HybridChunkDocumentRequest r -> client.chunkSourceWithHybridChunker(r); * } * } + * + *

Because {@link #toBuilder()} is available on the base type, a source (or target) can be + * injected once — polymorphically — before dispatching, without needing to know the concrete type: + * + *

{@code
+ * DocumentRequest withSource = request.toBuilder().source(source).build();
+ * }
*/ @JsonInclude(JsonInclude.Include.NON_EMPTY) @tools.jackson.databind.annotation.JsonDeserialize(builder = DocumentRequest.DocumentRequestBuilder.class) @@ -63,6 +70,14 @@ public abstract sealed class DocumentRequest @Nullable private Target target; + /** + * Returns a builder pre-populated with this request's current field values, allowing a modified + * copy to be created. Each concrete subtype returns its own builder covariantly. + * + * @return a builder initialized from this request + */ + public abstract DocumentRequest.Builder toBuilder(); + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") public abstract static class DocumentRequestBuilder> { } diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java index dc2a180e..49ce0cdc 100644 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java @@ -87,6 +87,44 @@ void toBuilderPreservesSourcesAndTarget() { assertThat(copy.getTarget()).isSameAs(original.getTarget()); } + @Test + void toBuilderReachableThroughBaseType() { + var target = InBodyTarget.builder().build(); + + DocumentRequest original = ConvertDocumentRequest.builder() + .target(target) + .build(); + + DocumentRequest withSource = original.toBuilder() + .source(HTTP_SOURCE) + .build(); + + assertThat(withSource.getSources()) + .singleElement() + .isEqualTo(HTTP_SOURCE); + + assertThat(withSource.getTarget()).isSameAs(target); + } + + @Test + void toBuilderThroughBaseTypePreservesConcreteType() { + List requests = List.of( + ConvertDocumentRequest.builder().source(HTTP_SOURCE).build(), BatchConvertDocumentRequest.builder().source(HTTP_SOURCE).target(ZipTarget.builder().build()) + .build(), HierarchicalChunkDocumentRequest.builder().source(HTTP_SOURCE).build(), HybridChunkDocumentRequest.builder().source(HTTP_SOURCE).build() + ); + + var rebuiltClasses = requests.stream() + .map(request -> request.toBuilder().build()) + .map(Object::getClass) + .toList(); + + var originalClasses = requests.stream() + .map(Object::getClass) + .toList(); + + assertThat(rebuiltClasses).isEqualTo(originalClasses); + } + @Test void batchConvertGetTargetThrowsWhenTargetIsNull() { BatchConvertDocumentRequest request = BatchConvertDocumentRequest.builder() diff --git a/docs/src/doc/docs/whats-new.md b/docs/src/doc/docs/whats-new.md index fa69c7d6..d58c48af 100644 --- a/docs/src/doc/docs/whats-new.md +++ b/docs/src/doc/docs/whats-new.md @@ -27,6 +27,7 @@ Docling Java {{ gradle.project_version }} includes important breaking changes, a * **New `DocumentRequest` sealed base class** — `ConvertDocumentRequest`, `BatchConvertDocumentRequest`, and `ChunkDocumentRequest` now extend a common `DocumentRequest` abstract class in the `ai.docling.serve.api.request` package. This enables polymorphism when working with different request types — for example, accepting a `DocumentRequest` and dispatching to the correct endpoint based on the concrete type via pattern matching. * **New `ProcessedDocumentResponse` sealed base class** — `ConvertDocumentResponse` and `ChunkDocumentResponse` now extend a common `ProcessedDocumentResponse` abstract class in the `ai.docling.serve.api.response` package. This enables polymorphic handling of document processing responses — for example, using `ProcessedDocumentResponse` as a type bound in generic APIs that work with both conversion and chunking results. +* **`toBuilder()` on the `DocumentRequest` base type** — `DocumentRequest` (and the intermediate `ChunkDocumentRequest`) now expose `toBuilder()`, so a request can be cloned and modified through the base type without first pattern-matching on the concrete subtype. This makes it possible to inject a `source` or `target` once — polymorphically — before dispatching, e.g. `request.toBuilder().source(source).build()`. ### 0.6.1