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);
}
}