diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java
new file mode 100644
index 00000000..280d1115
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java
@@ -0,0 +1,73 @@
+package ai.docling.serve.api.convert.response;
+
+import java.net.URI;
+import java.time.Instant;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents a reference to a single output artifact returned as a presigned URL.
+ *
+ *
Serialization uses {@link JsonInclude.Include#NON_EMPTY}, so nulls and empty
+ * fields are omitted from JSON output.
+ */
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@tools.jackson.databind.annotation.JsonDeserialize(builder = ArtifactRef.Builder.class)
+@lombok.extern.jackson.Jacksonized
+@lombok.Builder(toBuilder = true)
+@lombok.Getter
+@lombok.ToString
+public class ArtifactRef {
+
+ /**
+ * Export format of the artifact.
+ *
+ * @param artifactType the artifact type
+ * @return the artifact type
+ */
+ @JsonProperty("artifact_type")
+ private ArtifactType artifactType;
+
+ /**
+ * MIME type of the artifact content.
+ *
+ * @param mimeType the MIME type
+ * @return the MIME type
+ */
+ @JsonProperty("mime_type")
+ private String mimeType;
+
+ /**
+ * Presigned URL used to download the artifact.
+ *
+ * @param uri the presigned URL
+ * @return the presigned URL
+ */
+ @JsonProperty("uri")
+ private URI uri;
+
+ /**
+ * Instant at which the presigned URL signature stops being valid.
+ *
+ * @param urlExpiresAt the expiry timestamp
+ * @return the expiry timestamp
+ */
+ @JsonProperty("url_expires_at")
+ private Instant urlExpiresAt;
+
+ /**
+ * Builder for creating {@link ArtifactRef} instances.
+ * Generated by Lombok's {@code @Builder} annotation.
+ *
+ * Builder methods:
+ *
+ * - {@code artifactType(ArtifactType)} - Set the artifact type
+ * - {@code mimeType(String)} - Set the MIME type
+ * - {@code uri(URI)} - Set the presigned URL
+ * - {@code urlExpiresAt(Instant)} - Set the expiry timestamp
+ *
+ */
+ @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "")
+ public static class Builder { }
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java
new file mode 100644
index 00000000..29a4a540
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java
@@ -0,0 +1,25 @@
+package ai.docling.serve.api.convert.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents the output format of a converted-document artifact.
+ * Each value is mapped to a specific JSON property for serialization and deserialization.
+ *
+ *
+ * - {@code JSON}: Serialized {@code DoclingDocument} JSON.
+ * - {@code HTML}: HTML rendering of the document.
+ * - {@code MARKDOWN}: Markdown rendering of the document.
+ * - {@code TEXT}: Plain-text rendering of the document.
+ * - {@code DOCTAGS}: DocTags rendering of the document.
+ * - {@code RESOURCE_BUNDLE}: ZIP archive containing extracted images and supporting resources.
+ *
+ */
+public enum ArtifactType {
+ @JsonProperty("json") JSON,
+ @JsonProperty("html") HTML,
+ @JsonProperty("markdown") MARKDOWN,
+ @JsonProperty("text") TEXT,
+ @JsonProperty("doctags") DOCTAGS,
+ @JsonProperty("resource_bundle") RESOURCE_BUNDLE;
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java
new file mode 100644
index 00000000..f35f8366
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java
@@ -0,0 +1,25 @@
+package ai.docling.serve.api.convert.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents the possible conversion outcomes for a document.
+ * Each status is mapped to a specific JSON property for serialization and deserialization.
+ *
+ *
+ * - {@code PENDING}: Indicates that the conversion has not yet started.
+ * - {@code STARTED}: Indicates that the conversion is currently in progress.
+ * - {@code SUCCESS}: Indicates that all pages of the document were converted.
+ * - {@code PARTIAL_SUCCESS}: Indicates that some pages were converted and others failed.
+ * - {@code FAILURE}: Indicates that the document could not be converted.
+ * - {@code SKIPPED}: Indicates that the document was rejected at admission.
+ *
+ */
+public enum ConversionStatus {
+ @JsonProperty("pending") PENDING,
+ @JsonProperty("started") STARTED,
+ @JsonProperty("success") SUCCESS,
+ @JsonProperty("partial_success") PARTIAL_SUCCESS,
+ @JsonProperty("failure") FAILURE,
+ @JsonProperty("skipped") SKIPPED;
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java
index 8b7dc49a..a36104b4 100644
--- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java
@@ -18,10 +18,11 @@
@JsonSubTypes({
@JsonSubTypes.Type(InBodyConvertDocumentResponse.class),
@JsonSubTypes.Type(PreSignedUrlConvertDocumentResponse.class),
+ @JsonSubTypes.Type(PreSignedUrlConvertResponse.class),
@JsonSubTypes.Type(ZipArchiveConvertDocumentResponse.class)
})
public abstract sealed class ConvertDocumentResponse permits InBodyConvertDocumentResponse, PreSignedUrlConvertDocumentResponse,
- ZipArchiveConvertDocumentResponse {
+ PreSignedUrlConvertResponse, ZipArchiveConvertDocumentResponse {
/**
* Type of response
*
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java
new file mode 100644
index 00000000..9952297d
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java
@@ -0,0 +1,114 @@
+package ai.docling.serve.api.convert.response;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
+
+/**
+ * Represents the conversion outcome and artifact references for a single source document.
+ *
+ * Serialization uses {@link JsonInclude.Include#NON_EMPTY}, so nulls and empty
+ * collections are omitted from JSON output.
+ */
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@tools.jackson.databind.annotation.JsonDeserialize(builder = DocumentArtifactItem.Builder.class)
+@lombok.extern.jackson.Jacksonized
+@lombok.Builder(toBuilder = true)
+@lombok.Getter
+@lombok.ToString
+public class DocumentArtifactItem {
+
+ /**
+ * Zero-based index of the source document within the originating task.
+ *
+ * @param sourceIndex the source index
+ * @return the source index
+ */
+ @JsonProperty("source_index")
+ private Integer sourceIndex;
+
+ /**
+ * Canonical identifier of the source document.
+ *
+ * @param sourceUri the source URI
+ * @return the source URI
+ */
+ @JsonProperty("source_uri")
+ private String sourceUri;
+
+ /**
+ * Filename used as the stem of each output artifact.
+ *
+ * @param filename the source filename
+ * @return the source filename
+ */
+ @JsonProperty("filename")
+ private String filename;
+
+ /**
+ * Terminal conversion outcome for this document.
+ *
+ * @param status the conversion status
+ * @return the conversion status
+ */
+ @JsonProperty("status")
+ private ConversionStatus status;
+
+ /**
+ * Errors encountered while converting this document.
+ *
+ * @param errors the list of errors
+ * @return the list of errors
+ */
+ @JsonProperty("errors")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ @lombok.Singular
+ private List errors;
+
+ /**
+ * Per-stage timing breakdown keyed by stage name.
+ *
+ * @param timings the timings map
+ * @return the timings map
+ */
+ @JsonProperty("timings")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ @lombok.Singular
+ private Map timings;
+
+ /**
+ * Presigned URLs for each requested output format.
+ *
+ * @param artifacts the list of artifact references
+ * @return the list of artifact references
+ */
+ @JsonProperty("artifacts")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ @lombok.Singular
+ private List artifacts;
+
+ /**
+ * Builder for creating {@link DocumentArtifactItem} instances.
+ * Generated by Lombok's {@code @Builder} annotation.
+ *
+ * Builder methods:
+ *
+ * - {@code sourceIndex(Integer)} - Set the source index
+ * - {@code sourceUri(String)} - Set the source URI
+ * - {@code filename(String)} - Set the source filename
+ * - {@code status(ConversionStatus)} - Set the conversion status
+ * - {@code errors(List)} - Set the list of errors
+ * - {@code error(ErrorItem)} - Add a single error (use with @Singular)
+ * - {@code timings(Map)} - Set the timings map
+ * - {@code timing(String, ProfilingItem)} - Add a single timing entry (use with @Singular)
+ * - {@code artifacts(List)} - Set the list of artifact references
+ * - {@code artifact(ArtifactRef)} - Add a single artifact reference (use with @Singular)
+ *
+ */
+ @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "")
+ public static class Builder { }
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java
new file mode 100644
index 00000000..586e50be
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java
@@ -0,0 +1,116 @@
+package ai.docling.serve.api.convert.response;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
+
+/**
+ * Response for document conversions that target server-managed presigned URLs.
+ *
+ * This response type is returned when the conversion request specifies a
+ * {@link ai.docling.serve.api.convert.request.target.PresignedUrlTarget}. Each
+ * source document is represented by a {@link DocumentArtifactItem} in
+ * {@link #getDocuments() documents} which carries its conversion outcome and
+ * the list of presigned URLs the server produced for the requested output
+ * formats.
+ *
+ * Serialization uses {@link JsonInclude.Include#NON_EMPTY}, so nulls and empty
+ * collections/strings are omitted from JSON output.
+ *
+ * @see ConvertDocumentResponse
+ * @see ResponseType#PRE_SIGNED_URL_RESPONSE
+ * @see ai.docling.serve.api.convert.request.target.PresignedUrlTarget
+ * @see DocumentArtifactItem
+ */
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@tools.jackson.databind.annotation.JsonDeserialize(builder = PreSignedUrlConvertResponse.Builder.class)
+@lombok.extern.jackson.Jacksonized
+@lombok.Builder(toBuilder = true)
+@lombok.Getter
+@lombok.ToString
+public final class PreSignedUrlConvertResponse extends ConvertDocumentResponse {
+
+ /**
+ * Total processing time in seconds.
+ *
+ * @param processingTime the processing time in seconds
+ * @return the processing time in seconds
+ */
+ @JsonProperty("processing_time")
+ private Double processingTime;
+
+ /**
+ * Number of attempted conversions.
+ *
+ * @param numConverted the number of attempted conversions
+ * @return the number of attempted conversions
+ */
+ @JsonProperty("num_converted")
+ private Integer numConverted;
+
+ /**
+ * Number of successful conversions.
+ *
+ * @param numSucceeded the number of successful conversions
+ * @return the number of successful conversions
+ */
+ @JsonProperty("num_succeeded")
+ private Integer numSucceeded;
+
+ /**
+ * Number of partial successes.
+ *
+ * @param numPartiallySucceeded the number of partial successes
+ * @return the number of partial successes
+ */
+ @JsonProperty("num_partially_succeeded")
+ private Integer numPartiallySucceeded;
+
+ /**
+ * Number of failed conversions.
+ *
+ * @param numFailed the number of failed conversions
+ * @return the number of failed conversions
+ */
+ @JsonProperty("num_failed")
+ private Integer numFailed;
+
+ /**
+ * Per-source conversion outcomes and presigned artifact URLs.
+ *
+ * @param documents the list of per-source artifact items
+ * @return the list of per-source artifact items
+ */
+ @JsonProperty("documents")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ @lombok.Singular
+ private List documents;
+
+ @Override
+ @lombok.ToString.Include
+ public ResponseType getResponseType() {
+ return ResponseType.PRE_SIGNED_URL_RESPONSE;
+ }
+
+ /**
+ * Builder for creating {@link PreSignedUrlConvertResponse} instances.
+ * Generated by Lombok's {@code @Builder} annotation.
+ *
+ * Builder methods:
+ *
+ * - {@code processingTime(Double)} - Set the total processing time in seconds
+ * - {@code numConverted(Integer)} - Set the number of attempted conversions
+ * - {@code numSucceeded(Integer)} - Set the number of successful conversions
+ * - {@code numPartiallySucceeded(Integer)} - Set the number of partial successes
+ * - {@code numFailed(Integer)} - Set the number of failed conversions
+ * - {@code documents(List)} - Set the list of per-source artifact items
+ * - {@code document(DocumentArtifactItem)} - Add a single per-source artifact item (use with @Singular)
+ *
+ */
+ @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "")
+ public static class Builder { }
+
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java
new file mode 100644
index 00000000..383000e4
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java
@@ -0,0 +1,81 @@
+package ai.docling.serve.api.convert.response;
+
+import java.time.Instant;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
+
+/**
+ * Represents per-stage timing measurements produced during a document conversion.
+ *
+ * Serialization uses {@link JsonInclude.Include#NON_EMPTY}, so nulls and empty
+ * collections are omitted from JSON output.
+ */
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+@tools.jackson.databind.annotation.JsonDeserialize(builder = ProfilingItem.Builder.class)
+@lombok.extern.jackson.Jacksonized
+@lombok.Builder(toBuilder = true)
+@lombok.Getter
+@lombok.ToString
+public class ProfilingItem {
+
+ /**
+ * Scope of the stage being measured.
+ *
+ * @param scope the profiling scope
+ * @return the profiling scope
+ */
+ @JsonProperty("scope")
+ private ProfilingScope scope;
+
+ /**
+ * Number of measurements recorded.
+ *
+ * @param count the measurement count
+ * @return the measurement count
+ */
+ @JsonProperty("count")
+ private Integer count;
+
+ /**
+ * Per-measurement durations in seconds.
+ *
+ * @param times the list of durations
+ * @return the list of durations
+ */
+ @JsonProperty("times")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ @lombok.Singular
+ private List times;
+
+ /**
+ * Start timestamps for each measurement.
+ *
+ * @param startTimestamps the list of start timestamps
+ * @return the list of start timestamps
+ */
+ @JsonProperty("start_timestamps")
+ @JsonSetter(nulls = Nulls.AS_EMPTY)
+ @lombok.Singular
+ private List startTimestamps;
+
+ /**
+ * Builder for creating {@link ProfilingItem} instances.
+ * Generated by Lombok's {@code @Builder} annotation.
+ *
+ * Builder methods:
+ *
+ * - {@code scope(ProfilingScope)} - Set the profiling scope
+ * - {@code count(Integer)} - Set the measurement count
+ * - {@code times(List)} - Set the list of durations
+ * - {@code time(Double)} - Add a single duration (use with @Singular)
+ * - {@code startTimestamps(List)} - Set the list of start timestamps
+ * - {@code startTimestamp(Instant)} - Add a single start timestamp (use with @Singular)
+ *
+ */
+ @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "")
+ public static class Builder { }
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java
new file mode 100644
index 00000000..ac33b0af
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java
@@ -0,0 +1,17 @@
+package ai.docling.serve.api.convert.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents the scope of a profiling measurement.
+ * Each value is mapped to a specific JSON property for serialization and deserialization.
+ *
+ *
+ * - {@code PAGE}: Indicates that the measurement is recorded per page.
+ * - {@code DOCUMENT}: Indicates that the measurement is recorded per document.
+ *
+ */
+public enum ProfilingScope {
+ @JsonProperty("page") PAGE,
+ @JsonProperty("document") DOCUMENT;
+}
diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java
index 608c2355..4e7ad254 100644
--- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java
+++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java
@@ -13,15 +13,19 @@
* ({@link InBodyConvertDocumentResponse})
* {@link #ZIP_ARCHIVE} - Content is packaged and returned as a ZIP archive
* ({@link ZipArchiveConvertDocumentResponse})
- * {@link #PRE_SIGNED_URL} - Content is packaged as a ZIP archive and uploaded to the given target URI
- * and statistical information is returned.
+ * {@link #PRE_SIGNED_URL} - Content is uploaded to a client-supplied remote target and
+ * only aggregate processing statistics are returned.
* ({@link PreSignedUrlConvertDocumentResponse})
+ * {@link #PRE_SIGNED_URL_RESPONSE} - Each output artifact is uploaded to the server's
+ * configured object storage and returned as a time-limited presigned URL grouped by source document.
+ * ({@link PreSignedUrlConvertResponse})
*
*
* @see ConvertDocumentResponse
* @see InBodyConvertDocumentResponse
* @see ZipArchiveConvertDocumentResponse
* @see PreSignedUrlConvertDocumentResponse
+ * @see PreSignedUrlConvertResponse
*/
public enum ResponseType {
@@ -44,5 +48,12 @@ public enum ResponseType {
*
*/
@JsonProperty("presigned_url")
- PRE_SIGNED_URL
+ PRE_SIGNED_URL,
+
+ /**
+ * Represents response type - {@link PreSignedUrlConvertResponse}
+ *
+ */
+ @JsonProperty("presigned_url_response")
+ PRE_SIGNED_URL_RESPONSE
}
diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java
new file mode 100644
index 00000000..35ce6f76
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java
@@ -0,0 +1,55 @@
+package ai.docling.serve.api.convert.response;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.time.Instant;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link ArtifactRef}.
+ */
+class ArtifactRefTests {
+
+ @Test
+ void whenBuiltWithAllFieldsThenGettersReturnSetValues() {
+ URI uri = URI.create("https://example.com/doc.json");
+ Instant expiresAt = Instant.parse("2026-06-15T11:22:41Z");
+
+ ArtifactRef ref = ArtifactRef.builder()
+ .artifactType(ArtifactType.JSON)
+ .mimeType("application/json")
+ .uri(uri)
+ .urlExpiresAt(expiresAt)
+ .build();
+
+ assertThat(ref.getArtifactType()).isEqualTo(ArtifactType.JSON);
+ assertThat(ref.getMimeType()).isEqualTo("application/json");
+ assertThat(ref.getUri()).isEqualTo(uri);
+ assertThat(ref.getUrlExpiresAt()).isEqualTo(expiresAt);
+ }
+
+ @Test
+ void whenBuiltWithNullFieldsThenGettersReturnNull() {
+ ArtifactRef ref = ArtifactRef.builder().build();
+
+ assertThat(ref.getArtifactType()).isNull();
+ assertThat(ref.getMimeType()).isNull();
+ assertThat(ref.getUri()).isNull();
+ assertThat(ref.getUrlExpiresAt()).isNull();
+ }
+
+ @Test
+ void toBuilderRoundtripsToEqualInstance() {
+ ArtifactRef original = ArtifactRef.builder()
+ .artifactType(ArtifactType.MARKDOWN)
+ .mimeType("text/markdown")
+ .uri(URI.create("https://example.com/doc.md"))
+ .build();
+
+ ArtifactRef roundtripped = original.toBuilder().build();
+
+ assertThat(roundtripped).usingRecursiveComparison().isEqualTo(original);
+ }
+}
diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java
index f6f95a73..b34eace1 100644
--- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java
+++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java
@@ -87,6 +87,54 @@ void createPreSignedUrlConvertDocumentResponseWithNullFields() {
assertThat(response.getNumSucceeded()).isNull();
}
+ @Test
+ void createPreSignedUrlConvertResponseWithNullFields() {
+ PreSignedUrlConvertResponse response = PreSignedUrlConvertResponse.builder().build();
+
+ assertThat(response.getNumConverted()).isNull();
+ assertThat(response.getNumFailed()).isNull();
+ assertThat(response.getNumPartiallySucceeded()).isNull();
+ assertThat(response.getNumSucceeded()).isNull();
+ assertThat(response.getProcessingTime()).isNull();
+ assertThat(response.getDocuments()).isNotNull().isEmpty();
+ }
+
+ @Test
+ void createPreSignedUrlConvertResponseWithAllFields() {
+ ArtifactRef jsonArtifact = ArtifactRef.builder()
+ .artifactType(ArtifactType.JSON)
+ .mimeType("application/json")
+ .uri(java.net.URI.create("https://example.com/doc.json"))
+ .urlExpiresAt(java.time.Instant.parse("2026-06-15T11:22:41Z"))
+ .build();
+ DocumentArtifactItem document = DocumentArtifactItem.builder()
+ .sourceIndex(0)
+ .sourceUri("https://example.com/example.pdf")
+ .filename("example.pdf")
+ .status(ConversionStatus.SUCCESS)
+ .artifact(jsonArtifact)
+ .build();
+
+ PreSignedUrlConvertResponse response = PreSignedUrlConvertResponse.builder()
+ .processingTime(2.41)
+ .numConverted(1)
+ .numSucceeded(1)
+ .numPartiallySucceeded(0)
+ .numFailed(0)
+ .document(document)
+ .build();
+
+ assertThat(response.getProcessingTime()).isEqualTo(2.41);
+ assertThat(response.getNumConverted()).isEqualTo(1);
+ assertThat(response.getNumSucceeded()).isEqualTo(1);
+ assertThat(response.getNumPartiallySucceeded()).isZero();
+ assertThat(response.getNumFailed()).isZero();
+ assertThat(response.getDocuments()).hasSize(1);
+ assertThat(response.getDocuments().get(0).getStatus()).isEqualTo(ConversionStatus.SUCCESS);
+ assertThat(response.getDocuments().get(0).getArtifacts()).hasSize(1);
+ assertThat(response.getDocuments().get(0).getArtifacts().get(0).getArtifactType()).isEqualTo(ArtifactType.JSON);
+ }
+
@Test
void createResponseWithEmptyCollections() {
DocumentResponse document = DocumentResponse.builder()
diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java
new file mode 100644
index 00000000..21093b70
--- /dev/null
+++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java
@@ -0,0 +1,78 @@
+package ai.docling.serve.api.convert.response;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link DocumentArtifactItem}.
+ */
+class DocumentArtifactItemTests {
+
+ @Test
+ void whenBuiltWithAllFieldsThenGettersReturnSetValues() {
+ ArtifactRef jsonRef = ArtifactRef.builder()
+ .artifactType(ArtifactType.JSON)
+ .mimeType("application/json")
+ .uri(URI.create("https://example.com/doc.json"))
+ .build();
+ ArtifactRef mdRef = ArtifactRef.builder()
+ .artifactType(ArtifactType.MARKDOWN)
+ .mimeType("text/markdown")
+ .uri(URI.create("https://example.com/doc.md"))
+ .build();
+ ErrorItem error = ErrorItem.builder()
+ .componentType("pipeline")
+ .errorMessage("page 3 malformed")
+ .moduleName("StandardPdfPipeline")
+ .build();
+
+ DocumentArtifactItem item = DocumentArtifactItem.builder()
+ .sourceIndex(0)
+ .sourceUri("https://example.com/example.pdf")
+ .filename("example.pdf")
+ .status(ConversionStatus.PARTIAL_SUCCESS)
+ .error(error)
+ .artifact(jsonRef)
+ .artifact(mdRef)
+ .build();
+
+ assertThat(item.getSourceIndex()).isZero();
+ assertThat(item.getSourceUri()).isEqualTo("https://example.com/example.pdf");
+ assertThat(item.getFilename()).isEqualTo("example.pdf");
+ assertThat(item.getStatus()).isEqualTo(ConversionStatus.PARTIAL_SUCCESS);
+ assertThat(item.getErrors()).containsExactly(error);
+ assertThat(item.getArtifacts()).containsExactly(jsonRef, mdRef);
+ assertThat(item.getTimings()).isNotNull().isEmpty();
+ }
+
+ @Test
+ void whenBuiltWithNullCollectionsThenGettersReturnEmpty() {
+ DocumentArtifactItem item = DocumentArtifactItem.builder()
+ .sourceIndex(0)
+ .filename("example.pdf")
+ .status(ConversionStatus.SUCCESS)
+ .build();
+
+ assertThat(item.getErrors()).isNotNull().isEmpty();
+ assertThat(item.getTimings()).isNotNull().isEmpty();
+ assertThat(item.getArtifacts()).isNotNull().isEmpty();
+ }
+
+ @Test
+ void errorsListIsImmutableAfterBuild() {
+ ErrorItem first = ErrorItem.builder().errorMessage("first").build();
+
+ DocumentArtifactItem item = DocumentArtifactItem.builder()
+ .filename("example.pdf")
+ .status(ConversionStatus.FAILURE)
+ .errors(List.of(first))
+ .build();
+
+ assertThat(item.getErrors()).containsExactly(first);
+ assertThat(item.getErrors()).isUnmodifiable();
+ }
+}