diff --git a/json-org/src/main/java/tools/jackson/datatype/jsonorg/JSONObjectDeserializer.java b/json-org/src/main/java/tools/jackson/datatype/jsonorg/JSONObjectDeserializer.java index e847187d..46f428ca 100644 --- a/json-org/src/main/java/tools/jackson/datatype/jsonorg/JSONObjectDeserializer.java +++ b/json-org/src/main/java/tools/jackson/datatype/jsonorg/JSONObjectDeserializer.java @@ -5,6 +5,7 @@ import tools.jackson.databind.*; import tools.jackson.databind.deser.std.StdDeserializer; import tools.jackson.databind.type.LogicalType; +import tools.jackson.databind.util.ClassUtil; import org.json.JSONException; import org.json.JSONObject; @@ -27,11 +28,21 @@ public LogicalType logicalType() { public JSONObject deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { - JSONObject ob = new JSONObject(); JsonToken t = p.currentToken(); - if (t == JsonToken.START_OBJECT) { + if (p.isExpectedStartObjectToken()) { t = p.nextToken(); + } else if (t != JsonToken.PROPERTY_NAME && t != JsonToken.END_OBJECT) { + // 09-Sep-2026, pjfanning: [datatypes-misc#90] Need to verify it IS an Object (like + // `JSONArrayDeserializer` does for Arrays); + // otherwise we would quietly return an empty JSONObject and leave the parser + // pointing in the middle of the (non-Object) value. + // But note: PROPERTY_NAME and END_OBJECT are legal too, since we may be + // called with parser already inside Object (e.g. after As-Property Type Id) + return (JSONObject) ctxt.handleUnexpectedToken(getValueType(ctxt), t, p, + "Unexpected token (%s), expected START_OBJECT for %s value", + t, ClassUtil.nameOf(handledType())); } + JSONObject ob = new JSONObject(); for (; t == JsonToken.PROPERTY_NAME; t = p.nextToken()) { String fieldName = p.currentName(); t = p.nextToken(); diff --git a/json-org/src/test/java/tools/jackson/datatype/jsonorg/FailOnNonObjectTest.java b/json-org/src/test/java/tools/jackson/datatype/jsonorg/FailOnNonObjectTest.java new file mode 100644 index 00000000..34f8c6d2 --- /dev/null +++ b/json-org/src/test/java/tools/jackson/datatype/jsonorg/FailOnNonObjectTest.java @@ -0,0 +1,99 @@ +package tools.jackson.datatype.jsonorg; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.json.JSONObject; + +import tools.jackson.core.type.TypeReference; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.exc.MismatchedInputException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests to verify that deserialization of {@link JSONObject} fails cleanly for + * non-Object input, instead of quietly returning an empty {@code JSONObject} + * (and, for structured input, leaving the parser mid-value). + */ +public class FailOnNonObjectTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + @Test + public void testFailOnNumber() throws Exception + { + try { + JSONObject ob = MAPPER.readValue("42", JSONObject.class); + fail("Should not pass but got: "+ob); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (VALUE_NUMBER_INT), expected START_OBJECT"); + } + } + + @Test + public void testFailOnString() throws Exception + { + try { + JSONObject ob = MAPPER.readValue(a2q("'abc'"), JSONObject.class); + fail("Should not pass but got: "+ob); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (VALUE_STRING), expected START_OBJECT"); + } + } + + @Test + public void testFailOnArray() throws Exception + { + try { + JSONObject ob = MAPPER.readValue("[1,2]", JSONObject.class); + fail("Should not pass but got: "+ob); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (START_ARRAY), expected START_OBJECT"); + } + } + + // Most importantly: bad element must not desync the parser for the ones that follow + @Test + public void testFailOnScalarWithinList() throws Exception + { + try { + List obs = MAPPER.readValue(a2q("[{'a':1},42,{'b':2}]"), + new TypeReference>() { }); + fail("Should not pass but got: "+obs); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (VALUE_NUMBER_INT), expected START_OBJECT"); + } + } + + @Test + public void testFailOnArrayWithinList() throws Exception + { + try { + List obs = MAPPER.readValue(a2q("[{'a':1},[7,8],{'b':2}]"), + new TypeReference>() { }); + fail("Should not pass but got: "+obs); + } catch (MismatchedInputException e) { + verifyException(e, "Unexpected token (START_ARRAY), expected START_OBJECT"); + } + } + + // But valid Objects must keep working, including empty ones + @Test + public void testEmptyObjectStillOk() throws Exception + { + assertEquals(0, MAPPER.readValue("{}", JSONObject.class).length()); + } + + @Test + public void testObjectListStillOk() throws Exception + { + List obs = MAPPER.readValue(a2q("[{'a':1},{'b':2}]"), + new TypeReference>() { }); + assertEquals(2, obs.size()); + assertEquals(1, obs.get(0).getInt("a")); + assertEquals(2, obs.get(1).getInt("b")); + } +} diff --git a/json-org/src/test/java/tools/jackson/datatype/jsonorg/ModuleTestBase.java b/json-org/src/test/java/tools/jackson/datatype/jsonorg/ModuleTestBase.java index 41974471..9a5a9756 100644 --- a/json-org/src/test/java/tools/jackson/datatype/jsonorg/ModuleTestBase.java +++ b/json-org/src/test/java/tools/jackson/datatype/jsonorg/ModuleTestBase.java @@ -30,6 +30,10 @@ public JsonMapper.Builder newMapperBuilder() { .addModule(new JsonOrgModule()); } + protected static String a2q(String json) { + return json.replace("'", "\""); + } + protected void verifyException(Throwable e, String... matches) { String msg = e.getMessage(); diff --git a/json-org/src/test/java/tools/jackson/datatype/jsonorg/TypeInformationTest.java b/json-org/src/test/java/tools/jackson/datatype/jsonorg/TypeInformationTest.java index 6b30f1e0..57c119db 100644 --- a/json-org/src/test/java/tools/jackson/datatype/jsonorg/TypeInformationTest.java +++ b/json-org/src/test/java/tools/jackson/datatype/jsonorg/TypeInformationTest.java @@ -24,7 +24,12 @@ public ObjectWrapper() { } private final ObjectMapper POLY_MAPPER = newMapperBuilder() .activateDefaultTyping(new NoCheckSubTypeValidator()) .build(); - + + private final ObjectMapper POLY_PROP_MAPPER = newMapperBuilder() + .activateDefaultTypingAsProperty(new NoCheckSubTypeValidator(), + DefaultTyping.NON_FINAL, "@class") + .build(); + @Test public void testWrappedArray() throws Exception { @@ -56,4 +61,35 @@ public void testWrappedObject() throws Exception assertEquals(1, resultOb.length()); assertTrue(resultOb.getBoolean("a")); } + + // [datatypes-misc#90]: with As-Property Type Id, deserializer is called with + // parser positioned past the Type Id: PROPERTY_NAME, or END_OBJECT for empty Object + @Test + public void testObjectWithTypeIdAsProperty() throws Exception + { + JSONObject ob = new JSONObject(); + ob.put("a", true); + + String json = POLY_PROP_MAPPER.writeValueAsString(new ObjectWrapper(ob)); + assertEquals(a2q("{'@class':'"+ObjectWrapper.class.getName()+"'," + +"'value':{'@class':'org.json.JSONObject','a':true}}"), json); + + ObjectWrapper result = POLY_PROP_MAPPER.readValue(json, ObjectWrapper.class); + assertEquals(JSONObject.class, result.value.getClass()); + JSONObject resultOb = (JSONObject) result.value; + assertEquals(1, resultOb.length()); + assertTrue(resultOb.getBoolean("a")); + } + + @Test + public void testEmptyObjectWithTypeIdAsProperty() throws Exception + { + String json = POLY_PROP_MAPPER.writeValueAsString(new ObjectWrapper(new JSONObject())); + assertEquals(a2q("{'@class':'"+ObjectWrapper.class.getName()+"'," + +"'value':{'@class':'org.json.JSONObject'}}"), json); + + ObjectWrapper result = POLY_PROP_MAPPER.readValue(json, ObjectWrapper.class); + assertEquals(JSONObject.class, result.value.getClass()); + assertEquals(0, ((JSONObject) result.value).length()); + } } diff --git a/release-notes/CREDITS b/release-notes/CREDITS index c1598704..3a1c9371 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -16,3 +16,9 @@ Christopher Smith (chrylis@github): author of `jakarta-mail` module (added in 2. * Contributed #76: (joda-money) Add field-level amount representation for Joda-Money (`@JodaMoney` annotation) (3.1.0) + +@pjfanning + +* Contributed #90: (json-org) `JSONObject` deserialization quietly returns empty Object + for non-Object input + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 6c9ef325..14f0327d 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -15,7 +15,9 @@ Modules: 3.3.0 (not yet released) -No changes since 3.2 +#90: (json-org) `JSONObject` deserialization quietly returns empty Object + for non-Object input + (contributed by @pjfanning) 3.2.2 (14-Aug-2026)