From ad4e72ca5abee4f960d6b66bc64db1247a1de523 Mon Sep 17 00:00:00 2001 From: Lars Hagen Date: Tue, 9 Sep 2025 12:07:43 +0200 Subject: [PATCH] add method copyCurrentStructureExact to JsonGenerator follows the format of copyCurrentEvent/copyCurrentEventExact --- .../fasterxml/jackson/core/JsonGenerator.java | 103 ++++++++++++++++++ .../jackson/core/write/GeneratorCopyTest.java | 35 ++++++ 2 files changed, 138 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java index bc14c6c06a..5a4950dc3b 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java @@ -2690,6 +2690,46 @@ public void copyCurrentStructure(JsonParser p) throws IOException } } + /** + * Same as {@link #copyCurrentStructure} with the exception that copying of numeric + * values tries to avoid any conversion losses; in particular for floating-point + * numbers. This usually matters when transcoding from textual format like JSON + * to a binary format. + * See {@link #_copyCurrentFloatValueExact} for details. + * + * @param p Parser that points to the value to copy + * + * @throws IOException if there is either an underlying I/O problem or encoding + * issue at format layer + * + * @since 2.21 + */ + public void copyCurrentStructureExact(JsonParser p) throws IOException + { + JsonToken t = p.currentToken(); + // Let's handle field-name separately first + int id = (t == null) ? ID_NOT_AVAILABLE : t.id(); + if (id == ID_FIELD_NAME) { + writeFieldName(p.currentName()); + t = p.nextToken(); + id = (t == null) ? ID_NOT_AVAILABLE : t.id(); + // fall-through to copy the associated value + } + switch (id) { + case ID_START_OBJECT: + writeStartObject(); + _copyCurrentContentsExact(p); + return; + case ID_START_ARRAY: + writeStartArray(); + _copyCurrentContentsExact(p); + return; + + default: + copyCurrentEventExact(p); + } + } + // @since 2.10 protected void _copyCurrentContents(JsonParser p) throws IOException { @@ -2753,6 +2793,69 @@ protected void _copyCurrentContents(JsonParser p) throws IOException } } + // @since 2.21 + protected void _copyCurrentContentsExact(JsonParser p) throws IOException + { + int depth = 1; + JsonToken t; + + // Mostly copied from `copyCurrentEventExact()`, but with added nesting counts + while ((t = p.nextToken()) != null) { + switch (t.id()) { + case ID_FIELD_NAME: + writeFieldName(p.currentName()); + break; + + case ID_START_ARRAY: + writeStartArray(); + ++depth; + break; + + case ID_START_OBJECT: + writeStartObject(); + ++depth; + break; + + case ID_END_ARRAY: + writeEndArray(); + if (--depth == 0) { + return; + } + break; + case ID_END_OBJECT: + writeEndObject(); + if (--depth == 0) { + return; + } + break; + + case ID_STRING: + _copyCurrentStringValue(p); + break; + case ID_NUMBER_INT: + _copyCurrentIntValue(p); + break; + case ID_NUMBER_FLOAT: + _copyCurrentFloatValueExact(p); + break; + case ID_TRUE: + writeBoolean(true); + break; + case ID_FALSE: + writeBoolean(false); + break; + case ID_NULL: + writeNull(); + break; + case ID_EMBEDDED_OBJECT: + writeObject(p.getEmbeddedObject()); + break; + default: + throw new IllegalStateException("Internal error: unknown current token, "+t); + } + } + } + /** * Method for copying current {@link JsonToken#VALUE_NUMBER_FLOAT} value; * overridable by format backend implementations. diff --git a/src/test/java/com/fasterxml/jackson/core/write/GeneratorCopyTest.java b/src/test/java/com/fasterxml/jackson/core/write/GeneratorCopyTest.java index 356fbfd77c..66514bfc86 100644 --- a/src/test/java/com/fasterxml/jackson/core/write/GeneratorCopyTest.java +++ b/src/test/java/com/fasterxml/jackson/core/write/GeneratorCopyTest.java @@ -100,4 +100,39 @@ void copyObjectTokens() gen.close(); assertEquals("{\"a\":1}", sw.toString()); } + + @Test + void copyNumericTokensExactly() + throws Exception + { + JsonFactory jf = JSON_F; + final String DOC = a2q("{ 'a':0.123456789123456789123456789, 'b':[" + + "{ 'c' : null, 'd' : 0.123456789123456789123456789 }] }"); + try(JsonParser jp = jf.createParser(new StringReader(DOC))) { + StringWriter sw = new StringWriter(); + try (JsonGenerator gen = jf.createGenerator(sw)) { + assertToken(JsonToken.START_OBJECT, jp.nextToken()); + gen.copyCurrentStructureExact(jp); + // which will advance parser to matching end Object + assertToken(JsonToken.END_OBJECT, jp.currentToken()); + } + + assertEquals( + a2q("{'a':0.123456789123456789123456789,'b':[" + + "{'c':null,'d':0.123456789123456789123456789}]}"), + sw.toString() + ); + } + + try(JsonParser jp = jf.createParser(new StringReader("0.123456789123456789123456789"))) { + StringWriter sw = new StringWriter(); + try (JsonGenerator gen = jf.createGenerator(sw)) { + assertToken(JsonToken.VALUE_NUMBER_FLOAT, jp.nextToken()); + gen.copyCurrentStructureExact(jp); + assertToken(JsonToken.VALUE_NUMBER_FLOAT, jp.currentToken()); + } + + assertEquals("0.123456789123456789123456789", sw.toString()); + } + } }