Skip to content

Commit 67af1ef

Browse files
authored
Report end of input after property name as MismatchedInputException (#6376)
1 parent 14d2092 commit 67af1ef

8 files changed

Lines changed: 211 additions & 8 deletions

File tree

‎release-notes/VERSION‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ Versions: 3.x (for earlier see VERSION-2.x)
185185
config override (raw `Map.Entry` now handled as `Map.Entry<Object,Object>` in all shapes)
186186
(reported by @Marti2203)
187187
(fix by @wozhenbang2004)
188+
#6376: End of input right after a property name (possible with `TokenBuffer`) reported
189+
as `DatabindException` wrapping `NullPointerException`, not `MismatchedInputException`
190+
(fix by @cowtowncoder, w/ Claude code)
188191

189192
3.2.5 (not yet released)
190193

‎src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,7 +1435,7 @@ protected Object _deserializeWithExternalTypeId(JsonParser p, DeserializationCon
14351435
JsonToken t = p.nextToken();
14361436
// [JACKSON-831]: may have property AND be used as external type id:
14371437
// [databind#1329]: if so, and visible=false, skip setting on bean
1438-
if (t.isScalarValue()
1438+
if ((t != null) && t.isScalarValue()
14391439
&& ext.handleTypePropertyValue(p, ctxt, p.currentName(), bean)) {
14401440
continue;
14411441
}
@@ -1539,7 +1539,7 @@ protected Object deserializeUsingPropertyBasedWithExternalTypeId(JsonParser p, D
15391539
}
15401540
// [databind#3045]: may have property AND be used as external type id:
15411541
// [databind#1329]: if so, and visible=false, skip buffering
1542-
if (t.isScalarValue()
1542+
if ((t != null) && t.isScalarValue()
15431543
&& ext.handleTypePropertyValue(p, ctxt, propName, null)) {
15441544
continue;
15451545
}

‎src/main/java/tools/jackson/databind/deser/bean/BuilderBasedDeserializer.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ protected Object deserializeWithExternalTypeId(JsonParser p,
969969
SettableBeanProperty prop = _propertiesByIndex[ix];
970970
JsonToken t = p.nextToken();
971971
// May have property AND be used as external type id:
972-
if (t.isScalarValue()) {
972+
if ((t != null) && t.isScalarValue()) {
973973
ext.handleTypePropertyValue(p, ctxt, p.currentName(), bean);
974974
}
975975
if (activeView != null && !prop.visibleInView(activeView)) {

‎src/main/java/tools/jackson/databind/deser/jdk/ByteBufferDeserializer.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ public LogicalType logicalType() {
2121

2222
@Override
2323
public ByteBuffer deserialize(JsonParser parser, DeserializationContext ctxt) throws JacksonException {
24+
// No token if input ended (possible with `TokenBuffer`)
25+
if (!parser.hasCurrentToken()) {
26+
return (ByteBuffer) ctxt.handleUnexpectedToken(getValueType(ctxt), parser);
27+
}
2428
byte[] b = parser.getBinaryValue();
2529
return ByteBuffer.wrap(b);
2630
}
2731

2832
@Override
2933
public ByteBuffer deserialize(JsonParser p, DeserializationContext ctxt, ByteBuffer intoValue) throws JacksonException {
34+
if (!p.hasCurrentToken()) {
35+
return (ByteBuffer) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
36+
}
3037
// Let's actually read in streaming manner...
3138
try (ByteBufferBackedOutputStream out = new ByteBufferBackedOutputStream(intoValue)) {
3239
p.readBinaryValue(ctxt.getBase64Variant(), out);

‎src/main/java/tools/jackson/databind/deser/jdk/FactoryBasedEnumDeserializer.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt)
148148
return ctxt.reportInputMismatch(targetType,
149149
"Input mismatch reading Enum %s: properties-based `@JsonCreator` (%s) expects Object Value, got %s (`JsonToken.%s`)",
150150
ClassUtil.getTypeDescription(targetType), _factory,
151-
JsonToken.valueDescFor(t), t.name());
151+
JsonToken.valueDescFor(t), t);
152152
}
153153
}
154154
// 12-Oct-2021, tatu: We really should only get here if and when String
@@ -178,7 +178,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt)
178178
return ctxt.reportInputMismatch(targetType,
179179
"Input mismatch reading Enum %s: properties-based `@JsonCreator` (%s) expects String Value, got %s (`JsonToken.%s`)",
180180
ClassUtil.getTypeDescription(targetType), _factory,
181-
JsonToken.valueDescFor(t), t.name());
181+
JsonToken.valueDescFor(t), t);
182182
} else {
183183
value = p.getValueAsString();
184184
}

‎src/main/java/tools/jackson/databind/deser/std/StdDeserializer.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,7 +1490,9 @@ protected final String _parseString(JsonParser p, DeserializationContext ctxt,
14901490
// allow coercions for other scalar types
14911491
// 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's
14921492
// "real" scalar
1493-
if (p.currentToken().isScalarValue()) {
1493+
// Also: no token at all if input ended (possible with `TokenBuffer`)
1494+
final JsonToken t = p.currentToken();
1495+
if ((t != null) && t.isScalarValue()) {
14941496
String text = p.getValueAsString();
14951497
if (text != null) {
14961498
return text;

‎src/main/java/tools/jackson/databind/util/TokenBuffer.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,8 +1233,10 @@ public void copyCurrentStructure(JsonParser p)
12331233
writeName(p.currentName());
12341234
t = p.nextToken();
12351235
// fall-through to copy the associated value
1236-
} else if (t == null) {
1237-
// 13-Dec-2023, tatu: For some unexpected EOF cases we may end up here, so:
1236+
}
1237+
// 13-Dec-2023, tatu: For some unexpected EOF cases we may end up here, so:
1238+
// (including input ending right after property name)
1239+
if (t == null) {
12381240
throw new UnexpectedEndOfInputException(p, null, "Unexpected end-of-input");
12391241
}
12401242

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package tools.jackson.databind.deser;
2+
3+
import java.math.BigDecimal;
4+
import java.math.BigInteger;
5+
import java.nio.ByteBuffer;
6+
import java.util.*;
7+
import java.util.concurrent.atomic.AtomicReference;
8+
import java.util.stream.Stream;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.MethodSource;
13+
14+
import com.fasterxml.jackson.annotation.JsonCreator;
15+
import com.fasterxml.jackson.annotation.JsonProperty;
16+
import com.fasterxml.jackson.annotation.JsonSubTypes;
17+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
18+
19+
import tools.jackson.core.JsonParser;
20+
import tools.jackson.core.ObjectReadContext;
21+
import tools.jackson.core.exc.UnexpectedEndOfInputException;
22+
import tools.jackson.core.type.TypeReference;
23+
import tools.jackson.databind.JsonNode;
24+
import tools.jackson.databind.ObjectMapper;
25+
import tools.jackson.databind.annotation.JsonDeserialize;
26+
import tools.jackson.databind.annotation.JsonPOJOBuilder;
27+
import tools.jackson.databind.exc.MismatchedInputException;
28+
import tools.jackson.databind.testutil.DatabindTestUtil;
29+
import tools.jackson.databind.util.TokenBuffer;
30+
31+
import static org.junit.jupiter.api.Assertions.assertThrows;
32+
33+
/**
34+
* Token stream that ends right after a property name (possible with
35+
* {@link TokenBuffer}; parsers of textual formats fail on their own) leaves
36+
* value deserializers without a current token: must be reported as
37+
* a problem with input.
38+
*/
39+
class EndOfInputAfterPropertyNameTest extends DatabindTestUtil
40+
{
41+
static class Wrapper<T> { public T value; }
42+
43+
static class IntBean { public int value; }
44+
static class LongBean { public long value; }
45+
static class ShortBean { public short value; }
46+
static class ByteBean { public byte value; }
47+
static class DoubleBean { public double value; }
48+
static class FloatBean { public float value; }
49+
static class BooleanBean { public boolean value; }
50+
static class CharBean { public char value; }
51+
52+
static class Point { public int x; }
53+
record PointRecord(int x) { }
54+
enum ABC { A, B, C }
55+
56+
enum DelegatingFactoryEnum {
57+
A;
58+
59+
@JsonCreator
60+
static DelegatingFactoryEnum of(String id) { return A; }
61+
}
62+
63+
enum PropsFactoryEnum {
64+
A;
65+
66+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
67+
static PropsFactoryEnum of(@JsonProperty("id") String id) { return A; }
68+
}
69+
70+
@JsonSubTypes(@JsonSubTypes.Type(value = PayloadImpl.class, name = "impl"))
71+
interface Payload { }
72+
static class PayloadImpl implements Payload { }
73+
74+
// Regular property of types with External Type Id:
75+
76+
static class ExternalIdBean {
77+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
78+
property = "type")
79+
public Payload payload;
80+
81+
public int value;
82+
}
83+
84+
static class ExternalIdCreatorBean {
85+
public int value;
86+
87+
@JsonCreator
88+
ExternalIdCreatorBean(@JsonProperty("payload")
89+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
90+
property = "type")
91+
Payload payload) { }
92+
}
93+
94+
@JsonDeserialize(builder = ExternalIdViaBuilder.Builder.class)
95+
static class ExternalIdViaBuilder {
96+
@JsonPOJOBuilder(withPrefix = "")
97+
static class Builder {
98+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
99+
property = "type")
100+
public Payload payload;
101+
102+
public int value;
103+
104+
public ExternalIdViaBuilder build() { return new ExternalIdViaBuilder(); }
105+
}
106+
}
107+
108+
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
109+
@JsonSubTypes(@JsonSubTypes.Type(DeducedImpl.class))
110+
interface Deduced { }
111+
static class DeducedImpl implements Deduced { public int value; }
112+
113+
private final ObjectMapper MAPPER = newJsonMapper();
114+
115+
static Stream<TypeReference<?>> types() {
116+
return Stream.of(
117+
new TypeReference<IntBean>() { }, new TypeReference<LongBean>() { },
118+
new TypeReference<ShortBean>() { }, new TypeReference<ByteBean>() { },
119+
new TypeReference<DoubleBean>() { }, new TypeReference<FloatBean>() { },
120+
new TypeReference<BooleanBean>() { }, new TypeReference<CharBean>() { },
121+
new TypeReference<Wrapper<String>>() { }, new TypeReference<Wrapper<Integer>>() { },
122+
new TypeReference<Wrapper<Long>>() { }, new TypeReference<Wrapper<Short>>() { },
123+
new TypeReference<Wrapper<Byte>>() { }, new TypeReference<Wrapper<Double>>() { },
124+
new TypeReference<Wrapper<Float>>() { }, new TypeReference<Wrapper<Boolean>>() { },
125+
new TypeReference<Wrapper<Character>>() { },
126+
new TypeReference<Wrapper<BigDecimal>>() { }, new TypeReference<Wrapper<BigInteger>>() { },
127+
new TypeReference<Wrapper<UUID>>() { }, new TypeReference<Wrapper<Date>>() { },
128+
new TypeReference<Wrapper<Calendar>>() { }, new TypeReference<Wrapper<ABC>>() { },
129+
new TypeReference<Wrapper<Locale>>() { }, new TypeReference<Wrapper<Class<?>>>() { },
130+
new TypeReference<Wrapper<StringBuilder>>() { },
131+
new TypeReference<Wrapper<Object>>() { }, new TypeReference<Wrapper<JsonNode>>() { },
132+
new TypeReference<Wrapper<Point>>() { }, new TypeReference<Wrapper<PointRecord>>() { },
133+
new TypeReference<Wrapper<List<String>>>() { }, new TypeReference<Wrapper<List<Integer>>>() { },
134+
new TypeReference<Wrapper<Set<ABC>>>() { }, new TypeReference<Wrapper<EnumSet<ABC>>>() { },
135+
new TypeReference<Wrapper<Map<String, Integer>>>() { },
136+
new TypeReference<Wrapper<EnumMap<ABC, String>>>() { },
137+
new TypeReference<Wrapper<String[]>>() { }, new TypeReference<Wrapper<Object[]>>() { },
138+
new TypeReference<Wrapper<int[]>>() { }, new TypeReference<Wrapper<long[]>>() { },
139+
new TypeReference<Wrapper<boolean[]>>() { }, new TypeReference<Wrapper<double[]>>() { },
140+
new TypeReference<Wrapper<byte[]>>() { }, new TypeReference<Wrapper<char[]>>() { },
141+
new TypeReference<Wrapper<ByteBuffer>>() { },
142+
new TypeReference<Wrapper<DelegatingFactoryEnum>>() { },
143+
new TypeReference<Wrapper<PropsFactoryEnum>>() { },
144+
new TypeReference<ExternalIdBean>() { },
145+
new TypeReference<ExternalIdCreatorBean>() { },
146+
new TypeReference<ExternalIdViaBuilder>() { },
147+
new TypeReference<Wrapper<Optional<String>>>() { },
148+
new TypeReference<Wrapper<AtomicReference<String>>>() { },
149+
// and name directly within Map
150+
new TypeReference<Map<String, String>>() { },
151+
new TypeReference<Map<String, Integer>>() { },
152+
new TypeReference<Map<String, Object>>() { },
153+
new TypeReference<EnumMap<ABC, String>>() { },
154+
new TypeReference<Object>() { },
155+
new TypeReference<JsonNode>() { }
156+
);
157+
}
158+
159+
@ParameterizedTest
160+
@MethodSource("types")
161+
void endOfInputAfterName(TypeReference<?> type) throws Exception
162+
{
163+
// Name that is valid for all types tested: "value" for beans, and Map
164+
// keys need to match Enum in case of `EnumMap`
165+
final boolean enumKey = type.getType().getTypeName().startsWith("java.util.EnumMap");
166+
_verifyFailure(MismatchedInputException.class, type, enumKey ? "A" : "value");
167+
}
168+
169+
// Unlike above, content gets buffered via `TokenBuffer.copyCurrentStructure()`
170+
// which has its own exception type for the problem
171+
@Test
172+
void endOfInputAfterNameWithDeduction() throws Exception
173+
{
174+
_verifyFailure(UnexpectedEndOfInputException.class, new TypeReference<Deduced>() { }, "value");
175+
}
176+
177+
private void _verifyFailure(Class<? extends Exception> expFailure, TypeReference<?> type,
178+
String name) throws Exception
179+
{
180+
try (TokenBuffer buf = TokenBuffer.forGeneration()) {
181+
buf.writeStartObject();
182+
buf.writeName(name);
183+
try (JsonParser p = buf.asParser(ObjectReadContext.empty())) {
184+
assertThrows(expFailure,
185+
() -> MAPPER.readerFor(MAPPER.constructType(type)).readValue(p));
186+
}
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)