From b4315341da58e99ee23d68475d4f9c14237acb93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:34:15 +0000 Subject: [PATCH 1/4] Initial plan From d99431d9c143938bb1b7b818f22d21e5af7a018a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:41:12 +0000 Subject: [PATCH 2/4] fix(core): preserve empty-string text/orig fields during serialization Add @JsonInclude(NON_NULL) on text and orig fields in all DoclingDocument inner classes that had the class-level @JsonInclude(NON_EMPTY) causing empty strings to be silently dropped during serialization. Affected classes: TitleItem, SectionHeaderItem, FieldHeadingItem, ListItem, CodeItem, FormulaItem, TextItem, FieldValueItem, EntityMention, SummaryMetaField, CodeMetaField, DescriptionMetaField, TableCell, GraphCell. Add tests for empty-string text/orig serialization and roundtrip. Closes #598 --- .../java/ai/docling/core/DoclingDocument.java | 23 ++++++++ .../ai/docling/core/DoclingDocumentTests.java | 55 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java index 450fe963..a47b049c 100644 --- a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java +++ b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java @@ -311,6 +311,7 @@ public static class EntityMention { private String createdBy; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("orig") @@ -456,6 +457,7 @@ public static class SummaryMetaField { private String createdBy; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") @@ -641,9 +643,11 @@ public static final class TitleItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -703,9 +707,11 @@ public static final class SectionHeaderItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -768,9 +774,11 @@ public static final class FieldHeadingItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -833,9 +841,11 @@ public static final class ListItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -902,9 +912,11 @@ public static final class CodeItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -987,9 +999,11 @@ public static final class FormulaItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -1049,9 +1063,11 @@ public static final class TextItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -1111,9 +1127,11 @@ public static final class FieldValueItem implements BaseTextItem { private List prov; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("formatting") @@ -1283,6 +1301,7 @@ public static class CodeMetaField { private String createdBy; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("language") @@ -1309,6 +1328,7 @@ public static class DescriptionMetaField { private String createdBy; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") @@ -1660,6 +1680,7 @@ public static class TableCell { private Integer endColOffsetIdx; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("column_header") @@ -1794,9 +1815,11 @@ public static class GraphCell { private Integer cellId; @JsonProperty("text") + @JsonInclude(JsonInclude.Include.NON_NULL) private String text; @JsonProperty("orig") + @JsonInclude(JsonInclude.Include.NON_NULL) private String orig; @JsonProperty("prov") diff --git a/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java b/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java index 124e94ed..da8d685a 100644 --- a/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java +++ b/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java @@ -554,4 +554,59 @@ void shouldRejectNonTrackKindOnTrackSource() { assertThat(source.getKind()).isEqualTo("track"); } + @Test + void shouldSerializeEmptyStringTextAndOrigFieldsForTextItems() throws Exception { + // Regression test for: @JsonInclude(NON_EMPTY) making DoclingDocument serialization lossy. + // Empty string text/orig (e.g. formula with do_formula_enrichment=false) must be preserved. + ObjectMapper mapper = new ObjectMapper(); + + DoclingDocument document = DoclingDocument.builder() + .name("formula-document") + .text(DoclingDocument.FormulaItem.builder() + .selfRef("#/texts/0") + .contentLayer(DoclingDocument.ContentLayer.BODY) + .label(DocItemLabel.FORMULA) + .orig("") + .text("") + .build()) + .build(); + + String json = mapper.writeValueAsString(document); + + assertThat(json).contains("\"text\":\"\""); + assertThat(json).contains("\"orig\":\"\""); + } + + @Test + void shouldRoundtripEmptyStringTextThroughDeserializationAndSerialization() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + String json = """ + { + "name": "formula-document", + "texts": [ + { + "self_ref": "#/texts/0", + "content_layer": "body", + "label": "formula", + "orig": "", + "text": "" + } + ] + } + """; + + DoclingDocument document = mapper.readValue(json, DoclingDocument.class); + + assertThat(document.getTexts()).hasSize(1); + assertThat(document.getTexts().get(0)).isInstanceOf(DoclingDocument.FormulaItem.class); + DoclingDocument.FormulaItem formulaItem = (DoclingDocument.FormulaItem) document.getTexts().get(0); + assertThat(formulaItem.getText()).isEmpty(); + assertThat(formulaItem.getOrig()).isEmpty(); + + String serialized = mapper.writeValueAsString(document); + + assertThat(serialized).contains("\"text\":\"\""); + assertThat(serialized).contains("\"orig\":\"\""); + } + } From 2224e058076bb777f66851525cd4b55a534ee222 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:41:12 +0000 Subject: [PATCH 3/4] fix(core): normalize empty-string text/orig to null during deserialization Add custom setter methods to the Lombok Builder inner classes of the 14 affected DoclingDocument classes so that any empty-string value provided for 'text' or 'orig' is converted to null before the object is built. The class-level @JsonInclude(NON_EMPTY) then naturally omits these null fields during serialization, keeping the wire format consistent. Affected classes: TitleItem, SectionHeaderItem, FieldHeadingItem, ListItem, CodeItem, FormulaItem, TextItem, FieldValueItem, EntityMention, SummaryMetaField, CodeMetaField, DescriptionMetaField, TableCell, GraphCell. Also add @Nullable to all text/orig fields that lacked it (package is @NullMarked; these fields can legitimately be null after normalization). Closes #598 Signed-off-by: copilot-swe-agent[bot] --- .../java/ai/docling/core/DoclingDocument.java | 194 ++++++++++++++---- .../ai/docling/core/DoclingDocumentTests.java | 56 +++-- 2 files changed, 191 insertions(+), 59 deletions(-) diff --git a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java index a47b049c..036ea269 100644 --- a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java +++ b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java @@ -311,7 +311,7 @@ public static class EntityMention { private String createdBy; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("orig") @@ -332,7 +332,17 @@ public static class EntityMention { private List charspan; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } /** @@ -457,11 +467,16 @@ public static class SummaryMetaField { private String createdBy; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonTypeInfo( @@ -643,11 +658,11 @@ public static final class TitleItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -669,7 +684,17 @@ public static final class TitleItem implements BaseTextItem { private List comments; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -707,11 +732,11 @@ public static final class SectionHeaderItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -736,7 +761,17 @@ public static final class SectionHeaderItem implements BaseTextItem { private Integer level; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -774,11 +809,11 @@ public static final class FieldHeadingItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -803,7 +838,17 @@ public static final class FieldHeadingItem implements BaseTextItem { private Integer level; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -841,11 +886,11 @@ public static final class ListItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -874,7 +919,17 @@ public static final class ListItem implements BaseTextItem { private String marker; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -912,11 +967,11 @@ public static final class CodeItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -961,7 +1016,17 @@ public static final class CodeItem implements BaseTextItem { private String codeLanguage; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -999,11 +1064,11 @@ public static final class FormulaItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -1025,7 +1090,17 @@ public static final class FormulaItem implements BaseTextItem { private List comments; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1063,11 +1138,11 @@ public static final class TextItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -1089,7 +1164,17 @@ public static final class TextItem implements BaseTextItem { private List comments; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1127,11 +1212,11 @@ public static final class FieldValueItem implements BaseTextItem { private List prov; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("formatting") @@ -1157,7 +1242,17 @@ public static final class FieldValueItem implements BaseTextItem { private String kind; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1301,7 +1396,7 @@ public static class CodeMetaField { private String createdBy; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("language") @@ -1309,7 +1404,12 @@ public static class CodeMetaField { private String language; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1328,11 +1428,16 @@ public static class DescriptionMetaField { private String createdBy; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1680,7 +1785,7 @@ public static class TableCell { private Integer endColOffsetIdx; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("column_header") @@ -1700,7 +1805,12 @@ public static class TableCell { private RefItem ref; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1815,11 +1925,11 @@ public static class GraphCell { private Integer cellId; @JsonProperty("text") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String text; @JsonProperty("orig") - @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable private String orig; @JsonProperty("prov") @@ -1831,7 +1941,17 @@ public static class GraphCell { private RefItem itemRef; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + public Builder orig(String orig) { + this.orig = (orig != null && orig.isEmpty()) ? null : orig; + return this; + } + + public Builder text(String text) { + this.text = (text != null && text.isEmpty()) ? null : text; + return this; + } + } } public enum GraphLinkLabel { diff --git a/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java b/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java index da8d685a..71bdb794 100644 --- a/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java +++ b/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java @@ -555,32 +555,48 @@ void shouldRejectNonTrackKindOnTrackSource() { } @Test - void shouldSerializeEmptyStringTextAndOrigFieldsForTextItems() throws Exception { - // Regression test for: @JsonInclude(NON_EMPTY) making DoclingDocument serialization lossy. - // Empty string text/orig (e.g. formula with do_formula_enrichment=false) must be preserved. - ObjectMapper mapper = new ObjectMapper(); - - DoclingDocument document = DoclingDocument.builder() - .name("formula-document") - .text(DoclingDocument.FormulaItem.builder() + void shouldNormalizeEmptyStringTextAndOrigToNullViaBuilder() { + DoclingDocument.FormulaItem item = + DoclingDocument.FormulaItem.builder() .selfRef("#/texts/0") .contentLayer(DoclingDocument.ContentLayer.BODY) .label(DocItemLabel.FORMULA) .orig("") .text("") - .build()) - .build(); + .build(); + + assertThat(item.getText()).isNull(); + assertThat(item.getOrig()).isNull(); + } + + @Test + void shouldOmitTextAndOrigFromSerializationWhenNormalizedToNull() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + DoclingDocument document = + DoclingDocument.builder() + .name("formula-document") + .text( + DoclingDocument.FormulaItem.builder() + .selfRef("#/texts/0") + .contentLayer(DoclingDocument.ContentLayer.BODY) + .label(DocItemLabel.FORMULA) + .orig("") + .text("") + .build()) + .build(); String json = mapper.writeValueAsString(document); - assertThat(json).contains("\"text\":\"\""); - assertThat(json).contains("\"orig\":\"\""); + assertThat(json).doesNotContain("\"text\""); + assertThat(json).doesNotContain("\"orig\""); } @Test - void shouldRoundtripEmptyStringTextThroughDeserializationAndSerialization() throws Exception { + void shouldDeserializeEmptyStringTextAndOrigAsNull() throws Exception { ObjectMapper mapper = new ObjectMapper(); - String json = """ + String json = + """ { "name": "formula-document", "texts": [ @@ -599,14 +615,10 @@ void shouldRoundtripEmptyStringTextThroughDeserializationAndSerialization() thro assertThat(document.getTexts()).hasSize(1); assertThat(document.getTexts().get(0)).isInstanceOf(DoclingDocument.FormulaItem.class); - DoclingDocument.FormulaItem formulaItem = (DoclingDocument.FormulaItem) document.getTexts().get(0); - assertThat(formulaItem.getText()).isEmpty(); - assertThat(formulaItem.getOrig()).isEmpty(); - - String serialized = mapper.writeValueAsString(document); - - assertThat(serialized).contains("\"text\":\"\""); - assertThat(serialized).contains("\"orig\":\"\""); + DoclingDocument.FormulaItem formulaItem = + (DoclingDocument.FormulaItem) document.getTexts().get(0); + assertThat(formulaItem.getText()).isNull(); + assertThat(formulaItem.getOrig()).isNull(); } } From 937abd2c4088a1a2c2116cc98b2242df286cf37e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:52:02 +0000 Subject: [PATCH 4/4] fix(core): use Jackson deserializers to normalize empty-string text/orig to null Replace 14 custom builder setter methods with Jackson-native EmptyStringToNullJackson2Deserializer / EmptyStringToNullJackson3Deserializer inner classes. The affected text and orig fields are annotated with @JsonDeserialize(using = ...) for both Jackson 2 and 3, so empty strings in JSON are normalized to null during deserialization. Serialization continues to omit both null and empty-string values via the class-level @JsonInclude(NON_EMPTY). --- .../java/ai/docling/core/DoclingDocument.java | 235 ++++++++---------- .../ai/docling/core/DoclingDocumentTests.java | 10 +- 2 files changed, 107 insertions(+), 138 deletions(-) diff --git a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java index 036ea269..9af6279a 100644 --- a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java +++ b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java @@ -312,10 +312,14 @@ public static class EntityMention { @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("label") @@ -332,17 +336,7 @@ public static class EntityMention { private List charspan; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } /** @@ -468,15 +462,12 @@ public static class SummaryMetaField { @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonTypeInfo( @@ -659,10 +650,14 @@ public static final class TitleItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -684,17 +679,7 @@ public static final class TitleItem implements BaseTextItem { private List comments; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -733,10 +718,14 @@ public static final class SectionHeaderItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -761,17 +750,7 @@ public static final class SectionHeaderItem implements BaseTextItem { private Integer level; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -810,10 +789,14 @@ public static final class FieldHeadingItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -838,17 +821,7 @@ public static final class FieldHeadingItem implements BaseTextItem { private Integer level; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -887,10 +860,14 @@ public static final class ListItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -919,17 +896,7 @@ public static final class ListItem implements BaseTextItem { private String marker; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -968,10 +935,14 @@ public static final class CodeItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -1016,17 +987,7 @@ public static final class CodeItem implements BaseTextItem { private String codeLanguage; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1065,10 +1026,14 @@ public static final class FormulaItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -1090,17 +1055,7 @@ public static final class FormulaItem implements BaseTextItem { private List comments; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1139,10 +1094,14 @@ public static final class TextItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -1164,17 +1123,7 @@ public static final class TextItem implements BaseTextItem { private List comments; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1213,10 +1162,14 @@ public static final class FieldValueItem implements BaseTextItem { @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("formatting") @@ -1242,17 +1195,7 @@ public static final class FieldValueItem implements BaseTextItem { private String kind; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1397,6 +1340,8 @@ public static class CodeMetaField { @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("language") @@ -1404,12 +1349,7 @@ public static class CodeMetaField { private String language; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1429,15 +1369,12 @@ public static class DescriptionMetaField { @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1786,6 +1723,8 @@ public static class TableCell { @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("column_header") @@ -1805,12 +1744,7 @@ public static class TableCell { private RefItem ref; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -1926,10 +1860,14 @@ public static class GraphCell { @JsonProperty("text") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String text; @JsonProperty("orig") @Nullable + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson2Deserializer.class) + @tools.jackson.databind.annotation.JsonDeserialize(using = DoclingDocument.EmptyStringToNullJackson3Deserializer.class) private String orig; @JsonProperty("prov") @@ -1941,17 +1879,7 @@ public static class GraphCell { private RefItem itemRef; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { - public Builder orig(String orig) { - this.orig = (orig != null && orig.isEmpty()) ? null : orig; - return this; - } - - public Builder text(String text) { - this.text = (text != null && text.isEmpty()) ? null : text; - return this; - } - } + public static class Builder { } } public enum GraphLinkLabel { @@ -2178,6 +2106,45 @@ public static class PageItem { public static class Builder { } } + + /** + * Jackson 2 deserializer that maps an empty string value to {@code null} + * for {@code text} and {@code orig} fields. + */ + static final class EmptyStringToNullJackson2Deserializer + extends com.fasterxml.jackson.databind.deser.std.StdDeserializer { + + EmptyStringToNullJackson2Deserializer() { + super(String.class); + } + + @Override + public String deserialize(com.fasterxml.jackson.core.JsonParser p, + com.fasterxml.jackson.databind.DeserializationContext ctx) throws java.io.IOException { + String value = p.getText(); + return (value == null || value.isEmpty()) ? null : value; + } + } + + /** + * Jackson 3 deserializer that maps an empty string value to {@code null} + * for {@code text} and {@code orig} fields. + */ + static final class EmptyStringToNullJackson3Deserializer + extends tools.jackson.databind.deser.std.StdDeserializer { + + EmptyStringToNullJackson3Deserializer() { + super(String.class); + } + + @Override + public String deserialize(tools.jackson.core.JsonParser p, + tools.jackson.databind.DeserializationContext ctx) throws tools.jackson.core.JacksonException { + String value = p.getText(); + return (value == null || value.isEmpty()) ? null : value; + } + } + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") public static class Builder { } diff --git a/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java b/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java index 71bdb794..d073e305 100644 --- a/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java +++ b/docling-core/src/test/java/ai/docling/core/DoclingDocumentTests.java @@ -555,7 +555,7 @@ void shouldRejectNonTrackKindOnTrackSource() { } @Test - void shouldNormalizeEmptyStringTextAndOrigToNullViaBuilder() { + void shouldKeepEmptyStringTextAndOrigViaBuilder() { DoclingDocument.FormulaItem item = DoclingDocument.FormulaItem.builder() .selfRef("#/texts/0") @@ -565,12 +565,14 @@ void shouldNormalizeEmptyStringTextAndOrigToNullViaBuilder() { .text("") .build(); - assertThat(item.getText()).isNull(); - assertThat(item.getOrig()).isNull(); + // Builder does not normalize empty strings; normalization happens via Jackson deserialization. + // Empty strings are still omitted from serialization by @JsonInclude(NON_EMPTY). + assertThat(item.getText()).isEmpty(); + assertThat(item.getOrig()).isEmpty(); } @Test - void shouldOmitTextAndOrigFromSerializationWhenNormalizedToNull() throws Exception { + void shouldOmitEmptyStringTextAndOrigFromSerialization() throws Exception { ObjectMapper mapper = new ObjectMapper(); DoclingDocument document =