Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<JSONObject> obs = MAPPER.readValue(a2q("[{'a':1},42,{'b':2}]"),
new TypeReference<List<JSONObject>>() { });
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<JSONObject> obs = MAPPER.readValue(a2q("[{'a':1},[7,8],{'b':2}]"),
new TypeReference<List<JSONObject>>() { });
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<JSONObject> obs = MAPPER.readValue(a2q("[{'a':1},{'b':2}]"),
new TypeReference<List<JSONObject>>() { });
assertEquals(2, obs.size());
assertEquals(1, obs.get(0).getInt("a"));
assertEquals(2, obs.get(1).getInt("b"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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());
}
}
6 changes: 6 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 3 additions & 1 deletion release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading