DoclingDocument Java Serialization — Lossy Round-Tripping#
JSON round-tripping through the Java DoclingDocument model is inherently lossy due to three compounding issues: empty-field suppression on write, silent field dropping on read, and a missing top-level furniture field. Any pipeline that deserializes a DoclingDocument from Python-produced JSON and then re-serializes it risks producing output that differs from — and is non-compliant with — the canonical schema.
Issue 1: @JsonInclude(NON_EMPTY) drops empty fields on serialization#
DoclingDocument and every inner class (e.g., GroupItem, TitleItem) are annotated with @JsonInclude(JsonInclude.Include.NON_EMPTY) at the class level. During serialization, Jackson omits any field whose value is null, an empty string, or an empty collection.
This is not overridable at the mapper level: ObjectMapper.setSerializationInclusion(Include.ALWAYS) does not win over a class-level @JsonInclude annotation. The only way to losslessly re-emit a deserialized document is to operate on the raw response bytes before the Java model ever parses them .
Concrete example (issue #598): Docling Serve preserves "text": "" on formula items when do_formula_enrichment=false. After Java deserialization the field is still empty string in memory, but the subsequent ObjectMapper.writeValueAsBytes(doc) call drops it. The downstream Python importer then fails validation because a required field is absent .
Issue 2: FAIL_ON_UNKNOWN_PROPERTIES=false silently drops unrecognized fields on deserialization#
The Jackson 2 client explicitly sets DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES to false. In Jackson 3, this is already the default (hence no explicit config in DoclingServeJackson3Client). The result is the same in both cases: any JSON field not mapped to a Java field is silently discarded during deserialization, with no warning or error.
This means that when the Python schema adds new fields to the wire format — or when a field is present in the JSON served by Docling Serve but absent from the Java model — that data disappears without trace after a Java round-trip.
Issue 3: furniture field missing from DoclingDocument#
The Python DoclingDocument schema defines furniture as a mandatory top-level GroupItem representing headers, footers, and other non-body content. It is present in every document produced by Docling Serve.
The Java DoclingDocument now declares a @Nullable GroupItem furniture field, but it is marked @Nullable — meaning when unset, it serializes to nothing under NON_EMPTY. In earlier releases (confirmed through v0.4.7) the field was entirely absent from the class. Combined with FAIL_ON_UNKNOWN_PROPERTIES=false, the Python-produced furniture object was silently discarded on deserialization and never written back on re-serialization — causing schema non-compliance and downstream failures such as broken Markdown conversion .
The ContentLayer.FURNITURE enum value was present all along , indicating an incomplete partial implementation where the per-item classification was modeled but the top-level container was not.
Expected vs. actual output (from issue #386):
Python / Docling Serve produces:
"furniture": {
"self_ref": "#/furniture",
"parent": null,
"children": [],
"content_layer": "furniture",
"name": "_root_",
"label": "unspecified"
}
Java round-trip drops the field entirely .
Affected classes and key source files#
| File | Relevance |
|---|---|
DoclingDocument.java | Root model; class-level @JsonInclude(NON_EMPTY) and @Nullable furniture field |
DocumentResponse.java | API response wrapper; also carries @JsonInclude(NON_EMPTY) at class level |
DoclingServeJackson2Client.java | Explicit FAIL_ON_UNKNOWN_PROPERTIES=false |
DoclingServeJackson3Client.java | FAIL_ON_UNKNOWN_PROPERTIES=false by Jackson 3 default |
Workarounds (until fixed upstream)#
- Intercept raw bytes: read the HTTP response body as
byte[]orJsonNodebefore passing it to the Java model. If you only need to forward the JSON or pass it to Python, never deserialize it intoDoclingDocumentat all. - Post-process with
JsonNode: deserialize tocom.fasterxml.jackson.databind.JsonNode(which preserves all fields including empty strings), re-injectfurnitureif missing, then forward. - Validate before re-serializing: if you must use the Java model, validate the output JSON against the canonical Python schema (e.g., via the published JSON Schema) to catch dropped fields before they reach downstream consumers.
Open issues#
- #598 —
@JsonInclude(NON_EMPTY)makesDoclingDocumentserialization lossy (open as of 2026-07-20) - #386 —
furniturefield missing fromDoclingDocumentJSON serialized via docling-java
Reference: Python canonical schema#
The authoritative schema is defined in docling_core/types/doc/document.py (Pydantic). Docling Serve always emits furniture as a non-null GroupItem. The Java model should match this exactly for correct interoperability.