diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java index bbff8d4165..a30d8ecfd2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java @@ -452,6 +452,16 @@ public enum DeserializationFeature implements ConfigFeature */ ACCEPT_FLOAT_AS_INT(true), + /** + * Feature that allow accepting a JSON substring using a string + * instead of throwing an exception. + *

+ * Feature is disabled by default. + * + * @since 2.20.0 + */ + ACCEPT_SUB_JSON_AS_STRING(false), + /** * Feature that determines standard deserialization mechanism used for * Enum values: if enabled, Enums are assumed to have been serialized using diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java index acfb21f32a..b8aa2ee743 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java @@ -1,14 +1,14 @@ package com.fasterxml.jackson.databind.deser.std; -import java.io.IOException; -import java.util.*; - import com.fasterxml.jackson.annotation.JsonFormat; - import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; - -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; @@ -19,6 +19,12 @@ import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.type.LogicalType; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; + /** * Specifically optimized version for {@link java.util.Collection}s * that contain String values; reason is that this is a very common @@ -197,6 +203,14 @@ public Collection deserialize(JsonParser p, DeserializationContext ctxt, if (_valueDeserializer != null) { return deserializeUsingCustom(p, ctxt, result, _valueDeserializer); } + + if (ctxt.isEnabled(DeserializationFeature.ACCEPT_SUB_JSON_AS_STRING)) { + JsonToken currentToken = p.currentToken(); + if (currentToken == JsonToken.START_OBJECT || currentToken == JsonToken.START_ARRAY) { + return deserializeUsingCustom(p, ctxt, result, StringDeserializer.instance); + } + } + try { while (true) { // First the common case: diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java index 188269fd34..3e2ce1660b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java @@ -1,13 +1,18 @@ package com.fasterxml.jackson.databind.deser.std; -import java.io.IOException; - -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.type.LogicalType; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + @JacksonStdImpl public class StringDeserializer extends StdScalarDeserializer // non-final since 2.9 { @@ -36,6 +41,81 @@ public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingExcep @Override public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + // disabled, execute default serialization + if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_SUB_JSON_AS_STRING)) { + return defaultDeserialize(p, ctxt); + } + + JsonToken currentToken = p.getCurrentToken(); + + // not a JSON substring, execute default serialization + if (currentToken != JsonToken.START_OBJECT && currentToken != JsonToken.START_ARRAY) { + return defaultDeserialize(p, ctxt); + } + + StringBuilder builder = new StringBuilder(); + Deque stack = new ArrayDeque<>(); + + builder.append(p.getText()); + stack.push(currentToken); + + final boolean isArray = currentToken == JsonToken.START_ARRAY; + while (!stack.isEmpty()) { + // an empty stack indicates that the current sub JSON string has been searched and completed + JsonToken nextToken = p.nextToken(); + if (isArray && nextToken == JsonToken.END_ARRAY || + !isArray && nextToken == JsonToken.END_OBJECT) { + stack.pop(); + } + if (isArray && nextToken == JsonToken.START_ARRAY || + !isArray && nextToken == JsonToken.START_OBJECT) { + stack.push(nextToken); + } + + // start the sub JSON string, add comma if necessary + if (nextToken.isStructStart()) { + appendCommaIfNecessary(builder).append(p.getText()); + } + + // end of sub JSON string, delete comma if necessary + else if (nextToken.isStructEnd()) { + deleteCommaIfNecessary(builder).append(p.getText()); + } + + // number, Boolean type, without double quotation marks + else if (nextToken.isNumeric() || nextToken.isBoolean()) { + builder.append(p.getText()); + } + + // other types automatically add double quotation marks + else { + appendCommaIfNecessary(builder).append('"').append(p.getText()).append('"'); + } + + // automatically add colon if field + if (nextToken == JsonToken.FIELD_NAME) { + builder.append(':'); + } + // automatically add commas if value + else if (nextToken.isScalarValue()) { + builder.append(','); + } + } + + return builder.toString(); + } + + // Since we can never have type info ("natural type"; String, Boolean, Integer, Double): + // (is it an error to even call this version?) + @Override + public String deserializeWithType(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) throws IOException { + return deserialize(p, ctxt); + } + + protected String defaultDeserialize(JsonParser p, + DeserializationContext ctxt) throws IOException { // The critical path: ensure we handle the common case first. if (p.hasToken(JsonToken.VALUE_STRING)) { @@ -48,11 +128,19 @@ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx return _parseString(p, ctxt, this); } - // Since we can never have type info ("natural type"; String, Boolean, Integer, Double): - // (is it an error to even call this version?) - @Override - public String deserializeWithType(JsonParser p, DeserializationContext ctxt, - TypeDeserializer typeDeserializer) throws IOException { - return deserialize(p, ctxt); + private static StringBuilder appendCommaIfNecessary(StringBuilder builder) { + char lastChar = builder.charAt(builder.length() - 1); + if (lastChar != '{' && lastChar != '[' && lastChar != ':' && lastChar != ',') { + builder.append(','); + } + return builder; + } + + private static StringBuilder deleteCommaIfNecessary(StringBuilder builder) { + int lastIndex = builder.length() - 1; + if (builder.charAt(lastIndex) == ',') { + builder.deleteCharAt(lastIndex); + } + return builder; } } diff --git a/src/test/java/com/fasterxml/jackson/databind/StringDeserializerTest.java b/src/test/java/com/fasterxml/jackson/databind/StringDeserializerTest.java new file mode 100644 index 0000000000..320bc4fd27 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/StringDeserializerTest.java @@ -0,0 +1,68 @@ +package com.fasterxml.jackson.databind; + +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * Test validation uses a string to accept JSON substrings + * instead of throwing exceptions by default + */ +public class StringDeserializerTest +{ + + @Test + public void acceptSubJsonTest() throws Exception { + String json = "{'name':'root'," + + "'child':{'name':'child'}," + + "'children':[{'name':'children'}]," + + "'childrenList':[{'name':'childrenList'}]}"; + ObjectMapper mapper = DatabindTestUtil.newJsonMapper() + .configure(DeserializationFeature.ACCEPT_SUB_JSON_AS_STRING, true); + TestPojo testPojo = mapper.readValue(DatabindTestUtil.a2q(json), TestPojo.class); + Assertions.assertEquals(testPojo.getChild(), "{\"name\":\"child\"}"); + Assertions.assertEquals(testPojo.getChildren(), "[{\"name\":\"children\"}]"); + Assertions.assertEquals(testPojo.getChildrenList().get(0), "{\"name\":\"childrenList\"}"); + } + + static class TestPojo { + private String name; + private String child; + private String children; + private List childrenList; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getChild() { + return child; + } + + public void setChild(String child) { + this.child = child; + } + + public String getChildren() { + return children; + } + + public void setChildren(String children) { + this.children = children; + } + + public List getChildrenList() { + return childrenList; + } + + public void setChildrenList(List childrenList) { + this.childrenList = childrenList; + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/cfg/DeserializationConfigTest.java b/src/test/java/com/fasterxml/jackson/databind/cfg/DeserializationConfigTest.java index 184d23d57e..70b966cf31 100644 --- a/src/test/java/com/fasterxml/jackson/databind/cfg/DeserializationConfigTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/cfg/DeserializationConfigTest.java @@ -1,17 +1,27 @@ package com.fasterxml.jackson.databind.cfg; -import java.util.Collections; - -import org.junit.jupiter.api.Test; - import com.fasterxml.jackson.annotation.JsonInclude; - import com.fasterxml.jackson.core.json.JsonReadFeature; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyName; import com.fasterxml.jackson.databind.introspect.ClassIntrospector; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.Test; + +import java.util.Collections; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class DeserializationConfigTest extends DatabindTestUtil { @@ -101,7 +111,7 @@ public void testEnumIndexes() for (DeserializationFeature f : DeserializationFeature.values()) { max = Math.max(max, f.ordinal()); } - if (max >= 31) { // 31 is actually ok; 32 not + if (max >= 32) { // 32 is actually ok; 33 not fail("Max number of DeserializationFeature enums reached: "+max); } }