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..9af6279a 100644 --- a/docling-core/src/main/java/ai/docling/core/DoclingDocument.java +++ b/docling-core/src/main/java/ai/docling/core/DoclingDocument.java @@ -311,10 +311,15 @@ public static class EntityMention { private String createdBy; @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") @@ -456,6 +461,9 @@ public static class SummaryMetaField { private String createdBy; @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 = "") @@ -641,9 +649,15 @@ public static final class TitleItem implements BaseTextItem { private List prov; @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") @@ -703,9 +717,15 @@ public static final class SectionHeaderItem implements BaseTextItem { private List prov; @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") @@ -768,9 +788,15 @@ public static final class FieldHeadingItem implements BaseTextItem { private List prov; @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") @@ -833,9 +859,15 @@ public static final class ListItem implements BaseTextItem { private List prov; @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") @@ -902,9 +934,15 @@ public static final class CodeItem implements BaseTextItem { private List prov; @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") @@ -987,9 +1025,15 @@ public static final class FormulaItem implements BaseTextItem { private List prov; @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") @@ -1049,9 +1093,15 @@ public static final class TextItem implements BaseTextItem { private List prov; @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") @@ -1111,9 +1161,15 @@ public static final class FieldValueItem implements BaseTextItem { private List prov; @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") @@ -1283,6 +1339,9 @@ public static class CodeMetaField { private String createdBy; @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") @@ -1309,6 +1368,9 @@ public static class DescriptionMetaField { private String createdBy; @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 = "") @@ -1660,6 +1722,9 @@ public static class TableCell { private Integer endColOffsetIdx; @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") @@ -1794,9 +1859,15 @@ public static class GraphCell { private Integer cellId; @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") @@ -2035,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 124e94ed..d073e305 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,73 @@ void shouldRejectNonTrackKindOnTrackSource() { assertThat(source.getKind()).isEqualTo("track"); } + @Test + void shouldKeepEmptyStringTextAndOrigViaBuilder() { + DoclingDocument.FormulaItem item = + DoclingDocument.FormulaItem.builder() + .selfRef("#/texts/0") + .contentLayer(DoclingDocument.ContentLayer.BODY) + .label(DocItemLabel.FORMULA) + .orig("") + .text("") + .build(); + + // 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 shouldOmitEmptyStringTextAndOrigFromSerialization() 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).doesNotContain("\"text\""); + assertThat(json).doesNotContain("\"orig\""); + } + + @Test + void shouldDeserializeEmptyStringTextAndOrigAsNull() 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()).isNull(); + assertThat(formulaItem.getOrig()).isNull(); + } + }