diff --git a/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBDBase.java b/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBDBase.java index 3701979ec..c1186f0c3 100644 --- a/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBDBase.java +++ b/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBDBase.java @@ -125,20 +125,41 @@ public void resolve(DeserializationContext ctxt) // public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException // public final Object deserialize(JsonParser p, DeserializationContext ctxt, Object bean) throws IOException;; -// public final Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException; + + // Non-Jackson exceptions (like ones from a `DeserializationProblemHandler`) get the + // property as their path; stock lets them through as they are. All routes to + // unknown-property handling come here, including those of inherited stock code. + @Override + protected void handleUnknownProperty(JsonParser p, DeserializationContext ctxt, + Object beanOrClass, String propName) + throws JacksonException + { + try { + super.handleUnknownProperty(p, ctxt, beanOrClass, propName); + } catch (JacksonException e) { // has its path already + throw e; + } catch (Exception e) { + throw wrapAndThrow(e, (beanOrClass == null) ? handledType() : beanOrClass, + propName, ctxt); + } + } protected final Object _deserializeDisordered(JsonParser p, DeserializationContext ctxt, Object bean) throws JacksonException { + // Input name can only differ from logical one with these + final boolean inputNames = _beanProperties.hasAliases() || _beanProperties.isCaseInsensitive(); for (int ix = p.currentNameMatch(_propNameMatcher); ; ix = p.nextNameMatch(_propNameMatcher)) { if (ix >= 0) { p.nextToken(); + // Report name as found in input, read at the value token as stock does + final String inputName = inputNames ? p.currentName() : null; SettableBeanProperty prop = _propsByIndex[ix]; try { prop.deserializeAndSet(p, ctxt, bean); } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); + wrapAndThrow(e, bean, _failedPropertyName(inputName, prop), ctxt); } continue; } diff --git a/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBeanDeserializer.java b/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBeanDeserializer.java index 80a230b05..0d06ce679 100644 --- a/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBeanDeserializer.java +++ b/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicBeanDeserializer.java @@ -110,38 +110,46 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) injectValues(ctxt, bean); } - // We also we have at least 6 properties, so roll out first few - SettableBeanProperty prop = _orderedProperties[0]; - do { - try { - if (p.nextName(_orderedPropertyNames[0])) { - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[1])) { - prop = _orderedProperties[1]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[2])) { - prop = _orderedProperties[2]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[3])) { - prop = _orderedProperties[3]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - break; // yay! We did it! - } - } - } - } - return _deserializeDisordered(p, ctxt, bean); - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); - } - } while (false); + // We also we have at least 6 properties, so roll out first few. Each with a call site + // of its own, for type profiles; parser calls outside of `try`, as stock has them + if (!p.nextName(_orderedPropertyNames[0])) { + return _deserializeDisordered(p, ctxt, bean); + } + p.nextToken(); + try { + _orderedProperties[0].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[0].getName(), ctxt); + } + if (!p.nextName(_orderedPropertyNames[1])) { + return _deserializeDisordered(p, ctxt, bean); + } + p.nextToken(); + try { + _orderedProperties[1].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[1].getName(), ctxt); + } + if (!p.nextName(_orderedPropertyNames[2])) { + return _deserializeDisordered(p, ctxt, bean); + } + p.nextToken(); + try { + _orderedProperties[2].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[2].getName(), ctxt); + } + if (!p.nextName(_orderedPropertyNames[3])) { + return _deserializeDisordered(p, ctxt, bean); + } + p.nextToken(); + try { + _orderedProperties[3].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[3].getName(), ctxt); + } for (int i = 4, len = _orderedProperties.length; i < len; ++i) { - prop = _orderedProperties[i]; if (!p.nextName(_orderedPropertyNames[i])) { // miss... if (p.currentToken() == JsonToken.END_OBJECT) { return bean; @@ -149,11 +157,11 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) // we likely point to FIELD_NAME still; offline return _deserializeDisordered(p, ctxt, bean); } - p.nextToken(); // skip field, returns value token + p.nextToken(); try { - prop.deserializeAndSet(p, ctxt, bean); + _orderedProperties[i].deserializeAndSet(p, ctxt, bean); } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); + throw wrapAndThrow(e, bean, _orderedProperties[i].getName(), ctxt); } } // also, need to ensure we get closing END_OBJECT... @@ -185,145 +193,52 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object bean if (_externalTypeIdHandler != null) { return deserializeWithExternalTypeId(p, ctxt, bean); } - // We also we have at least 6 properties, so roll out first few + // We also we have at least 6 properties, so roll out first few. Each with a call site + // of its own, for type profiles; parser calls outside of `try`, as stock has them SettableBeanProperty prop = _orderedProperties[0]; - do { - try { - if (p.isExpectedStartObjectToken()) { - if (!p.nextName(_orderedPropertyNames[0])) { - return _deserializeDisordered(p, ctxt, bean); - } - } else if (!p.hasToken(JsonToken.PROPERTY_NAME) - || !prop.getName().equals(p.currentName())) { - return _deserializeDisordered(p, ctxt, bean); - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[1])) { - prop = _orderedProperties[1]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[2])) { - prop = _orderedProperties[2]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[3])) { - prop = _orderedProperties[3]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - break; // yay! We did it! - } - } - } + if (p.isExpectedStartObjectToken()) { + if (!p.nextName(_orderedPropertyNames[0])) { return _deserializeDisordered(p, ctxt, bean); - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); - } - } while (false); - - // then rest of properties - for (int i = 4, len = _orderedProperties.length; i < len; ++i) { - if (!p.nextName(_orderedPropertyNames[i])) { // miss... - if (p.hasToken(JsonToken.END_OBJECT)) { - return bean; - } - // we likely point to FIELD_NAME, so can just call parent impl - return super.deserialize(p, ctxt, bean); } - prop = _orderedProperties[i]; - p.nextToken(); // skip field, returns value token - try { - prop.deserializeAndSet(p, ctxt, bean); - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); - } - } - // also, need to ensure we get closing END_OBJECT... - if (p.nextToken() != JsonToken.END_OBJECT) { + } else if (!p.hasToken(JsonToken.PROPERTY_NAME)) { + // not an Object at all: stock leaves the bean as is return super.deserialize(p, ctxt, bean); + } else if (!prop.getName().equals(p.currentName())) { + return _deserializeDisordered(p, ctxt, bean); } - return bean; - } - - // much of below is cut'n pasted from BeanSerializer - @Override - public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) - throws JacksonException - { - // The ordered-property fast path below is only safe when no view filtering, - // non-standard creation or other non-vanilla features are in play; otherwise - // fall back to the parent implementation which handles all of those. - if (!_vanillaProcessing) { - return super.deserializeFromObject(p, ctxt); + p.nextToken(); + try { + prop.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, prop.getName(), ctxt); } - // See BeanDeserializer.deserializeFromObject [databind#622] - // Allow Object Id references to come in as JSON Objects as well... - if ((_objectIdReader != null) && _objectIdReader.maySerializeAsObject()) { - if (p.hasTokenId(JsonTokenId.ID_PROPERTY_NAME) - && _objectIdReader.isValidReferencePropertyName(p.currentName(), p)) { - return deserializeFromObjectId(p, ctxt); - } + if (!p.nextName(_orderedPropertyNames[1])) { + return _deserializeDisordered(p, ctxt, bean); } - if (_nonStandardCreation) { - if (_unwrappedPropertyHandler != null) { - return deserializeWithUnwrapped(p, ctxt); - } - if (_externalTypeIdHandler != null) { - return deserializeWithExternalTypeId(p, ctxt); - } - Object bean = deserializeFromObjectUsingNonDefault(p, ctxt); - if (_injectables != null) { - injectValues(ctxt, bean); - } - return bean; + p.nextToken(); + try { + _orderedProperties[1].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[1].getName(), ctxt); } - final Object bean = _valueInstantiator.createUsingDefault(ctxt); - // [databind#631]: Assign current value, to be accessible by custom serializers - p.assignCurrentValue(bean); - if (p.canReadObjectId()) { - Object id = p.getObjectId(); - if (id != null) { - _handleTypedObjectId(p, ctxt, bean, id); - } + if (!p.nextName(_orderedPropertyNames[2])) { + return _deserializeDisordered(p, ctxt, bean); } - if (_injectables != null) { - injectValues(ctxt, bean); + p.nextToken(); + try { + _orderedProperties[2].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[2].getName(), ctxt); + } + if (!p.nextName(_orderedPropertyNames[3])) { + return _deserializeDisordered(p, ctxt, bean); + } + p.nextToken(); + try { + _orderedProperties[3].deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _orderedProperties[3].getName(), ctxt); } - // We also we have at least 6 properties, so roll out first few - SettableBeanProperty prop = _orderedProperties[0]; - do { - try { - if (p.isExpectedStartObjectToken()) { - if (!p.nextName(_orderedPropertyNames[0])) { - return _deserializeDisordered(p, ctxt, bean); - } - } else if (!p.hasToken(JsonToken.PROPERTY_NAME) - || !prop.getName().equals(p.currentName())) { - return _deserializeDisordered(p, ctxt, bean); - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[1])) { - prop = _orderedProperties[1]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[2])) { - prop = _orderedProperties[2]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - if (p.nextName(_orderedPropertyNames[3])) { - prop = _orderedProperties[3]; - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - break; // yay! We did it! - } - } - } - return _deserializeDisordered(p, ctxt, bean); - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); - } - } while (false); // then rest of properties for (int i = 4, len = _orderedProperties.length; i < len; ++i) { @@ -334,18 +249,16 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) // we likely point to FIELD_NAME, so can just call parent impl return super.deserialize(p, ctxt, bean); } - prop = _orderedProperties[i]; - p.nextToken(); // skip field, returns value token + p.nextToken(); try { - prop.deserializeAndSet(p, ctxt, bean); + _orderedProperties[i].deserializeAndSet(p, ctxt, bean); } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); + throw wrapAndThrow(e, bean, _orderedProperties[i].getName(), ctxt); } } // also, need to ensure we get closing END_OBJECT... if (p.nextToken() != JsonToken.END_OBJECT) { - // [modules-base#343]: trailing (possibly unknown) properties remain - return _deserializeDisordered(p, ctxt, bean); + return super.deserialize(p, ctxt, bean); } return bean; } diff --git a/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicUnrolledDeserializer.java b/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicUnrolledDeserializer.java index 886a11686..6eb603bf5 100644 --- a/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicUnrolledDeserializer.java +++ b/afterburner/src/main/java/tools/jackson/module/afterburner/deser/SuperSonicUnrolledDeserializer.java @@ -179,65 +179,79 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) if (_injectables != null) { injectValues(ctxt, bean); } - SettableBeanProperty prop = null; - - try { - switch (_propCount) { - default: - //case 6: - prop = _prop1; - if (!p.nextName(_name1)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 5: - prop = _prop2; - if (!p.nextName(_name2)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 4: - prop = _prop3; - if (!p.nextName(_name3)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 3: - prop = _prop4; - if (!p.nextName(_name4)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 2: - prop = _prop5; - if (!p.nextName(_name5)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 1: - prop = _prop6; - if (!p.nextName(_name6)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // also, need to ensure we get closing END_OBJECT... - if (p.nextToken() == JsonToken.END_OBJECT) { - return bean; - } + // Each property with a call site of its own, for type profiles; parser calls + // outside of `try`, as stock has them + switch (_propCount) { + default: + //case 6: + if (!p.nextName(_name1)) { + break; + } + p.nextToken(); + try { + _prop1.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop1.getName(), ctxt); + } + // fall through + case 5: + if (!p.nextName(_name2)) { + break; + } + p.nextToken(); + try { + _prop2.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop2.getName(), ctxt); + } + // fall through + case 4: + if (!p.nextName(_name3)) { + break; + } + p.nextToken(); + try { + _prop3.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop3.getName(), ctxt); + } + // fall through + case 3: + if (!p.nextName(_name4)) { + break; + } + p.nextToken(); + try { + _prop4.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop4.getName(), ctxt); + } + // fall through + case 2: + if (!p.nextName(_name5)) { + break; + } + p.nextToken(); + try { + _prop5.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop5.getName(), ctxt); + } + // fall through + case 1: + if (!p.nextName(_name6)) { + break; + } + p.nextToken(); + try { + _prop6.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop6.getName(), ctxt); + } + // also, need to ensure we get closing END_OBJECT... + if (p.nextToken() == JsonToken.END_OBJECT) { + return bean; } - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); } // may have encountered pre-mature END_OBJECT too if (p.currentToken() == JsonToken.END_OBJECT) { @@ -283,184 +297,72 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object bean try { prop.deserializeAndSet(p, ctxt, bean); } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); + throw wrapAndThrow(e, bean, prop.getName(), ctxt); } // then rest of properties. NOTE! Off-by-one, to skip one we did above - try { - switch (_propCount) { - default: - //case 6: - prop = _prop2; - if (!p.nextName(_name2)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 5: - prop = _prop3; - if (!p.nextName(_name3)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 4: - prop = _prop4; - if (!p.nextName(_name4)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 3: - prop = _prop5; - if (!p.nextName(_name5)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 2: - prop = _prop6; - if (!p.nextName(_name6)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 1: - // NOTE! skip "last" one to compensate for OBO - // But we still need to ensure we get closing END_OBJECT... - if (p.nextToken() == JsonToken.END_OBJECT) { - return bean; - } + switch (_propCount) { + default: + //case 6: + if (!p.nextName(_name2)) { + break; } - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); - } - // may have encountered pre-mature END_OBJECT too - if (p.currentToken() == JsonToken.END_OBJECT) { - return bean; - } - return _deserializeDisordered(p, ctxt, bean); - } - - // much of below is cut'n pasted from BeanSerializer - @Override - public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) - throws JacksonException - { - // The ordered-property fast path below is only safe when no view filtering, - // non-standard creation or other non-vanilla features are in play; otherwise - // fall back to the parent implementation which handles all of those. - if (!_vanillaProcessing) { - return super.deserializeFromObject(p, ctxt); - } - // See BeanDeserializer.deserializeFromObject [databind#622] - // Allow Object Id references to come in as JSON Objects as well... - if ((_objectIdReader != null) && _objectIdReader.maySerializeAsObject()) { - if (p.hasTokenId(JsonTokenId.ID_PROPERTY_NAME) - && _objectIdReader.isValidReferencePropertyName(p.currentName(), p)) { - return deserializeFromObjectId(p, ctxt); + p.nextToken(); + try { + _prop2.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop2.getName(), ctxt); } - } - if (_nonStandardCreation) { - if (_unwrappedPropertyHandler != null) { - return deserializeWithUnwrapped(p, ctxt); + // fall through + case 5: + if (!p.nextName(_name3)) { + break; } - if (_externalTypeIdHandler != null) { - return deserializeWithExternalTypeId(p, ctxt); + p.nextToken(); + try { + _prop3.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop3.getName(), ctxt); } - Object bean = deserializeFromObjectUsingNonDefault(p, ctxt); - if (_injectables != null) { - injectValues(ctxt, bean); + // fall through + case 4: + if (!p.nextName(_name4)) { + break; } - return bean; - } - final Object bean = _valueInstantiator.createUsingDefault(ctxt); - // [databind#631]: Assign current value, to be accessible by custom serializers - p.assignCurrentValue(bean); - if (p.canReadObjectId()) { - Object id = p.getObjectId(); - if (id != null) { - _handleTypedObjectId(p, ctxt, bean, id); + p.nextToken(); + try { + _prop4.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop4.getName(), ctxt); } - } - if (_injectables != null) { - injectValues(ctxt, bean); - } - SettableBeanProperty prop = _orderedProperties[0]; - // First: verify that first name is expected - if (p.isExpectedStartObjectToken()) { - if (!p.nextName(_orderedPropertyNames[0])) { - return _deserializeDisordered(p, ctxt, bean); + // fall through + case 3: + if (!p.nextName(_name5)) { + break; } - } else if (!p.hasToken(JsonToken.PROPERTY_NAME) - || !prop.getName().equals(p.currentName())) { - return _deserializeDisordered(p, ctxt, bean); - } - // and deserialize - p.nextToken(); - try { - prop.deserializeAndSet(p, ctxt, bean); - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); - } - - // then rest of properties. NOTE! Off-by-one, to skip one we did above - try { - switch (_propCount) { - default: - //case 6: - prop = _prop2; - if (!p.nextName(_name2)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 5: - prop = _prop3; - if (!p.nextName(_name3)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 4: - prop = _prop4; - if (!p.nextName(_name4)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 3: - prop = _prop5; - if (!p.nextName(_name5)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 2: - prop = _prop6; - if (!p.nextName(_name6)) { - break; - } - p.nextToken(); - prop.deserializeAndSet(p, ctxt, bean); - // fall through - case 1: - // NOTE! skip "last" one to compensate for OBO - // But we still need to ensure we get closing END_OBJECT... - if (p.nextToken() == JsonToken.END_OBJECT) { - return bean; - } + p.nextToken(); + try { + _prop5.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop5.getName(), ctxt); + } + // fall through + case 2: + if (!p.nextName(_name6)) { + break; + } + p.nextToken(); + try { + _prop6.deserializeAndSet(p, ctxt, bean); + } catch (Exception e) { + throw wrapAndThrow(e, bean, _prop6.getName(), ctxt); + } + // fall through + case 1: + // NOTE! skip "last" one to compensate for OBO + // But we still need to ensure we get closing END_OBJECT... + if (p.nextToken() == JsonToken.END_OBJECT) { + return bean; } - } catch (Exception e) { - wrapAndThrow(e, bean, prop.getName(), ctxt); } // may have encountered pre-mature END_OBJECT too if (p.currentToken() == JsonToken.END_OBJECT) { diff --git a/afterburner/src/test/java/tools/jackson/module/afterburner/deser/AliasErrorPathTest.java b/afterburner/src/test/java/tools/jackson/module/afterburner/deser/AliasErrorPathTest.java new file mode 100644 index 000000000..3dec4d214 --- /dev/null +++ b/afterburner/src/test/java/tools/jackson/module/afterburner/deser/AliasErrorPathTest.java @@ -0,0 +1,232 @@ +package tools.jackson.module.afterburner.deser; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonMerge; +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.*; +import tools.jackson.databind.deser.DeserializationProblemHandler; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.module.afterburner.AfterburnerTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Path of an exception for a failing property must name the property as found + * in input (which may be an alias), and only once. + */ +public class AliasErrorPathTest extends AfterburnerTestBase +{ + // Up to 6 properties: SuperSonicUnrolledDeserializer + public static class SmallBean { + @JsonAlias("a1") public int a; + @JsonAlias("b1") public int b; + } + + // More than 6 properties: SuperSonicBeanDeserializer + public static class BigBean { + @JsonAlias("a1") public int a; + @JsonAlias("b1") public int b; + public int c, d, e, f; + @JsonAlias("g1") public int g; + } + + public static class PlainBigBean { + public int a, b, c, d, e, f, g; + } + + public static class CreatorBean { + final int a; + + @JsonCreator + public CreatorBean(@JsonProperty("a") int a) { this.a = a; } + } + + public record RecordBean(int a) { } + + public static class MergeHolder { + @JsonMerge public BigBean big = new BigBean(); + } + + private static final List> BEANS = List.of(SmallBean.class, BigBean.class); + + // Afterburner's optimized deserializers need "vanilla" processing + private final ObjectMapper MAPPER = _mapperBuilder().build(); + + @Test + public void aliasAndCanonicalName() throws Exception { + for (Class type : BEANS) { + ObjectReader r = MAPPER.readerFor(type); + _verifyPath(r, "{'a1':'bad'}", "a1"); + _verifyPath(r, "{'a':1,'b1':'bad'}", "b1"); + _verifyPath(r, "{'a':'bad'}", "a"); + // Not scalar: name must be found at these tokens too + _verifyPath(r, "{'a1':{}}", "a1"); + _verifyPath(r, "{'a1':[]}", "a1"); + } + } + + // Past the rolled out first properties of SuperSonicBeanDeserializer + @Test + public void laterProperties() throws Exception { + ObjectReader r = MAPPER.readerFor(BigBean.class); + _verifyPath(r, "{'a':1,'b':2,'c':3,'d':4,'e':5,'f':6,'g1':'bad'}", "g1"); + _verifyPath(r, "{'a':1,'b':2,'c':3,'d':4,'e':5,'f':6,'g':'bad'}", "g"); + } + + // No aliases, no case-insensitivity: input name is not read + @Test + public void noInputNames() throws Exception { + _verifyPath(MAPPER.readerFor(PlainBigBean.class), "{'b':'bad'}", "b"); + } + + @Test + public void caseInsensitive() throws Exception { + final ObjectMapper mapper = _mapperBuilder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + .build(); + for (Class type : BEANS) { + _verifyPath(mapper.readerFor(type), "{'A':'bad'}", "A"); + } + } + + @Test + public void forUpdate() throws Exception { + for (Object bean : List.of(new SmallBean(), new BigBean())) { + ObjectReader r = MAPPER.readerForUpdating(bean); + _verifyPath(r, "{'a1':'bad'}", "a1"); + _verifyPath(r, "{'a':1,'b1':'bad'}", "b1"); + } + } + + // Parser already pointing to the first property name + @Test + public void forUpdateFromPropertyName() throws Exception { + try (JsonParser p = MAPPER.createParser(a2q("{'a1':'bad'}"))) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + DatabindException e = assertThrows(DatabindException.class, + () -> MAPPER.readerForUpdating(new BigBean()).readValue(p)); + assertEquals(List.of("a1"), _path(e)); + assertTrue(_hasFrame(e, "_deserializeDisordered")); + } + } + + // Not an Object: nothing to update, bean is left as is, same as stock + @Test + public void updateFromNonObject() throws Exception { + for (ObjectMapper mapper : List.of(newVanillaJSONMapper(), MAPPER)) { + for (Object bean : List.of(new SmallBean(), new BigBean())) { + assertSame(bean, mapper.readerForUpdating(bean).readValue("12")); + } + assertNotNull(mapper.readValue(a2q("{'big':12}"), MergeHolder.class).big); + } + } + + // Failure is reported once, not also for earlier property + @Test + public void unknownProperty() throws Exception { + final ObjectMapper mapper = _mapperBuilder() + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + for (Class type : BEANS) { + _verifyPath(mapper.readerFor(type), "{'zzz':1}", "zzz"); + _verifyPath(mapper.readerFor(type), "{'a':1,'zzz':1}", "zzz"); + _verifyPath(mapper.readerForUpdating(new SmallBean()), "{'zzz':1}", "zzz"); + _verifyPath(mapper.readerForUpdating(new BigBean()), "{'zzz':1}", "zzz"); + } + } + + // Exception that a problem handler throws gets wrapped, with property in path + @Test + public void unknownPropertyHandlerFailure() throws Exception { + final DeserializationProblemHandler handler = new DeserializationProblemHandler() { + @Override + public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p, + ValueDeserializer deser, Object beanOrClass, String propName) { + throw new IllegalStateException("handler failure"); + } + }; + final ObjectMapper mapper = _mapperBuilder().addHandler(handler).build(); + for (String json : List.of("{'zzz':1}", "{'a':1,'zzz':1}", "{'a':1,'b':1,'zzz':1}")) { + _verifyWrapped(mapper.readerFor(SmallBean.class), json); + _verifyWrapped(mapper.readerForUpdating(new SmallBean()), json); + _verifyWrapped(mapper.readerFor(BigBean.class), json); + _verifyWrapped(mapper.readerForUpdating(new BigBean()), json); + } + // Past the rolled out first properties of SuperSonicBeanDeserializer, and past all + for (String json : List.of("{'a':1,'b':1,'c':1,'d':1,'e':1,'zzz':1}", + "{'a':1,'b':1,'c':1,'d':1,'e':1,'f':1,'g':1,'zzz':1}")) { + _verifyWrapped(mapper.readerFor(BigBean.class), json); + _verifyWrapped(mapper.readerForUpdating(new BigBean()), json); + } + // Other settings: not "vanilla", so goes through stock code that is inherited + final ObjectMapper nonVanilla = afterburnerMapperBuilder().addHandler(handler).build(); + _verifyWrapped(nonVanilla.readerFor(SmallBean.class), "{'a':1,'zzz':1}"); + _verifyWrapped(nonVanilla.readerFor(BigBean.class), "{'a':1,'zzz':1}"); + // With Creator, and record + for (Class type : List.of(CreatorBean.class, RecordBean.class)) { + _verifyWrapped(mapper.readerFor(type), "{'a':1,'zzz':1}"); + _verifyWrapped(nonVanilla.readerFor(type), "{'a':1,'zzz':1}"); + } + // Unless wrapping is disabled + assertThrows(IllegalStateException.class, () -> mapper.readerFor(BigBean.class) + .without(DeserializationFeature.WRAP_EXCEPTIONS) + .readValue(a2q("{'zzz':1}"))); + } + + // Parser failures are not failures of a property: no path, same as stock + @Test + public void parserFailures() throws Exception { + final ObjectMapper stock = newVanillaJSONMapper(); + for (String json : List.of("{ bad", "{'a':bad", "{'a':1, bad", "{'a':1,'b':bad", "{'b':1, bad")) { + for (ObjectReader r : List.of( + stock.readerFor(SmallBean.class), stock.readerFor(BigBean.class), + MAPPER.readerFor(SmallBean.class), MAPPER.readerFor(BigBean.class), + MAPPER.readerForUpdating(new SmallBean()), MAPPER.readerForUpdating(new BigBean()))) { + assertEquals(List.of(), _path(assertThrows(StreamReadException.class, + () -> r.readValue(a2q(json)))), json); + } + } + } + + private static JsonMapper.Builder _mapperBuilder() { + return afterburnerMapperBuilder() + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION); + } + + private static void _verifyWrapped(ObjectReader r, String json) { + DatabindException e = assertThrows(DatabindException.class, + () -> r.readValue(a2q(json)), json); + assertInstanceOf(IllegalStateException.class, e.getCause(), json); + assertEquals(List.of("zzz"), _path(e), json); + } + + private static void _verifyPath(ObjectReader r, String json, String expName) { + DatabindException e = assertThrows(DatabindException.class, + () -> r.readValue(a2q(json))); + assertEquals(List.of(expName), _path(e)); + assertTrue(_hasFrame(e, "deserialize"), "should go through Afterburner's deserializer"); + } + + private static List _path(JacksonException e) { + return e.getPath().stream() + .map(ref -> ref.getPropertyName()) + .toList(); + } + + private static boolean _hasFrame(Throwable e, String method) { + return Arrays.stream(e.getStackTrace()).anyMatch( + f -> f.getClassName().contains("SuperSonic") && f.getMethodName().equals(method)); + } +} diff --git a/afterburner/src/test/java/tools/jackson/module/afterburner/deser/UnknownProperty343Test.java b/afterburner/src/test/java/tools/jackson/module/afterburner/deser/UnknownProperty343Test.java index bd2b0d6e2..71d4f807c 100644 --- a/afterburner/src/test/java/tools/jackson/module/afterburner/deser/UnknownProperty343Test.java +++ b/afterburner/src/test/java/tools/jackson/module/afterburner/deser/UnknownProperty343Test.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonView; import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.module.afterburner.AfterburnerTestBase; @@ -13,14 +14,13 @@ // [modules-base#343]: SuperSonicBeanDeserializer.deserializeFromObject threw on a // trailing unknown property after all known ordered properties were consumed, even -// with FAIL_ON_UNKNOWN_PROPERTIES disabled. +// with FAIL_ON_UNKNOWN_PROPERTIES disabled. That override no longer exists: this +// stays as a regression test for the same input. public class UnknownProperty343Test extends AfterburnerTestBase { // Marker class used purely to force the bean deserializer to operate in // non-vanilla mode (view processing active), which routes through - // BeanDeserializer.deserializeFromObject rather than the vanilla fast path — - // and thus through SuperSonicBeanDeserializer.deserializeFromObject, which is - // where issue #343 lives. + // BeanDeserializer.deserializeFromObject rather than the vanilla fast path. static class DefaultView { } // 8 properties -> SuperSonicBeanDeserializer (threshold is > 6) @@ -36,21 +36,46 @@ public static class POJO343 { @JsonProperty("h") public String h; } + // No views: Afterburner's own ordered fast paths handle this one + public static class Vanilla343 { + public String a, b, c, d, e, f, g, h; + } + + private final static String JSON = "{" + + "\"a\":\"a\",\"b\":\"b\",\"c\":\"c\",\"d\":\"d\"," + + "\"e\":\"e\",\"f\":\"f\",\"g\":\"g\",\"h\":\"h\"," + + "\"i\":\"i\"}"; + private final ObjectMapper MAPPER = afterburnerMapperBuilder() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .build(); + // Afterburner's optimized deserializers need "vanilla" processing + private final ObjectMapper VANILLA_MAPPER = afterburnerMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .build(); + @Test public void testTrailingUnknownProperty() throws Exception { - String json = "{" - + "\"a\":\"a\",\"b\":\"b\",\"c\":\"c\",\"d\":\"d\"," - + "\"e\":\"e\",\"f\":\"f\",\"g\":\"g\",\"h\":\"h\"," - + "\"i\":\"i\"}"; POJO343 result = MAPPER.readerWithView(DefaultView.class) .forType(POJO343.class) - .readValue(json); + .readValue(JSON); assertEquals("a", result.a); assertEquals("h", result.h); } + + @Test + public void testTrailingUnknownPropertyVanilla() throws Exception + { + Vanilla343 result = VANILLA_MAPPER.readValue(JSON, Vanilla343.class); + assertEquals("a", result.a); + assertEquals("h", result.h); + + Vanilla343 bean = new Vanilla343(); + VANILLA_MAPPER.readerForUpdating(bean).readValue(JSON); + assertEquals("a", bean.a); + assertEquals("h", bean.h); + } } diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/ReaderCodegen.java b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/ReaderCodegen.java index 8b74ccc74..02ca2f8ab 100644 --- a/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/ReaderCodegen.java +++ b/blackbird/src/main/java/tools/jackson/module/blackbird/codegen/ReaderCodegen.java @@ -97,14 +97,11 @@ public static Construction record(MethodHandle ctor) { * only), which name the component locals in the debug table. * inputNames tells whether a matched name can differ from the * property's own name (aliases, case-insensitive matching), which - * builder-mode error messages then need. - * vanillaLoop (POJO mode only) tells whether stock reads the bean with - * its vanilla loop, which ends quietly on an odd token after an unknown - * name. + * error paths then need. */ record Plan(String className, List props, String[] stockName, String[] childName, String[] handleName, Mode mode, boolean injectables, - boolean vanillaLoop, ViewStrategy views, int[] matchArms, String[] localNames, boolean inputNames) {} + ViewStrategy views, int[] matchArms, String[] localNames, boolean inputNames) {} private ReaderCodegen() {} @@ -117,7 +114,7 @@ private ReaderCodegen() {} public static ValueDeserializer generate(Class beanClass, List props, PropertyNameMatcher matcher, int[] matchArms, BeanDeserializerBase source, BeanPropertyMap beanProps, Construction construction, boolean injectables, - boolean vanillaLoop, ViewStrategy views, String[] localNames) + ViewStrategy views, String[] localNames) throws ReflectiveOperationException { Map classData = new LinkedHashMap<>(); classData.put("matcher", matcher); @@ -164,7 +161,7 @@ public static ValueDeserializer generate(Class beanClass, List generateWithStockMatcher( } return ReaderCodegen.generate(beanClass, props, beanProps.getNameMatcher(), matchArms, delegate, beanProps, construction, stock.injectables(), - (builderClass == null) && stock.vanilla(), viewStrategy(ctxt, delegate, props, declaresViews), null); } @@ -391,7 +389,7 @@ private static ValueDeserializer generateRecord(BeanDeserializerBase del beanProps.initMatcher(ctxt.tokenStreamFactory()); RecordNames names = recordNames(ctxt, creator, beanProps, props); return ReaderCodegen.generate(beanClass, props, names.matcher(), names.matchArms(), - delegate, beanProps, Construction.record(recordCtor), false, false, + delegate, beanProps, Construction.record(recordCtor), false, viewStrategy(ctxt, delegate, props, declaresViews), localNames); } diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java index f359b96f4..b9c47146f 100644 --- a/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java +++ b/blackbird/src/main/java/tools/jackson/module/blackbird/deser/BBReaderPlaceholder.java @@ -50,7 +50,7 @@ public void resolve(DeserializationContext ctxt) { CodegenDebug.log(() -> "reader resolve " + handledType().getName()); super.resolve(ctxt); ValueDeserializer codec = BBReaderFactory.tryGenerate(new BBReaderFactory.Source( - this, _anySetter, _injectables != null, _vanillaProcessing, _beanProperties, + this, _anySetter, _injectables != null, _beanProperties, _propertyBasedCreator, _nonStandardCreation, (_delegateDeserializer != null) || (_arrayDelegateDeserializer != null), _externalTypeIdHandler != null, _unwrappedPropertyHandler != null, diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedBuilderBase.java b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedBuilderBase.java index 907197137..959c1fa3c 100644 --- a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedBuilderBase.java +++ b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedBuilderBase.java @@ -1,5 +1,6 @@ package tools.jackson.module.blackbird.internal; +import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.deser.SettableBeanProperty; @@ -59,35 +60,21 @@ protected final Object _finishBuild(DeserializationContext ctxt, Object builder) return finishBuild(ctxt, builder); } - // The property name that stock reports for a failing property: its - // general loop (deserializeFromObject) reports the name as written in - // the input (an alias, another case), its vanilla loop and view loop - // the property's own name. inputName is the name the generated arm read, - // or null for a bean without aliases or case-insensitive matching, where - // the two always agree; startObject tells whether the call entered on - // START_OBJECT (the stock vanilla loop's entry) or on a property name. - // Resolved here, on the failure path, so the entry pays nothing for it. - private String _failedName(SettableBeanProperty prop, String inputName, - boolean startObject, DeserializationContext ctxt) { - if ((inputName == null) - || (_vanillaProcessing && startObject) - || (_needViewProcesing && ctxt.getActiveView() != null)) { - return prop.getName(); - } - return inputName; - } - + // As in stock, a failing property is reported under the name as written + // in the input (an alias, another case). inputName is the name the + // generated arm read, or null for a bean without aliases or + // case-insensitive matching, where it always is the property's own name; + // resolved here, so the arms pay nothing for such beans. protected final RuntimeException _propertyException(Throwable t, Object builder, - SettableBeanProperty prop, String inputName, boolean startObject, - DeserializationContext ctxt) { - throw wrapAndThrow(t, builder, _failedName(prop, inputName, startObject, ctxt), ctxt); + SettableBeanProperty prop, String inputName, DeserializationContext ctxt) { + throw wrapAndThrow(t, builder, _failedPropertyName(inputName, prop), ctxt); } protected final RuntimeException _storeFailure(Throwable t, Object builder, Object childValue, SettableBeanProperty prop, JsonParser p, - String inputName, boolean startObject, DeserializationContext ctxt) { + String inputName, DeserializationContext ctxt) { throw wrapAndThrow(ReaderSupport.storeException(t, childValue, prop, p), builder, - _failedName(prop, inputName, startObject, ctxt), ctxt); + _failedPropertyName(inputName, prop), ctxt); } protected final void _injectValues(DeserializationContext ctxt, Object builder) { @@ -101,6 +88,23 @@ protected final void _handleUnknown(JsonParser p, DeserializationContext ctxt, handleUnknownVanilla(p, ctxt, builder, name); } + // Non-Jackson exceptions (like ones from a DeserializationProblemHandler) get + // the property as their path; stock lets them through as they are. All routes + // to unknown-property handling come here, including those of stock code the + // codec inherits, and buffered unknown properties of records. + @Override + protected void handleUnknownProperty(JsonParser p, DeserializationContext ctxt, + Object beanOrClass, String propName) { + try { + super.handleUnknownProperty(p, ctxt, beanOrClass, propName); + } catch (JacksonException e) { // has its path already + throw e; + } catch (Exception e) { + throw wrapAndThrow(e, (beanOrClass == null) ? handledType() : beanOrClass, + propName, ctxt); + } + } + // As in the stock vanilla loop, whatever the unexpected-token handling // returns is then built. protected final Object _unexpectedToken(JsonParser p, DeserializationContext ctxt, diff --git a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java index 34ef61ab7..32b12a9ba 100644 --- a/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java +++ b/blackbird/src/main/java/tools/jackson/module/blackbird/internal/GeneratedReaderBase.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.ValueDeserializer; @@ -94,21 +95,24 @@ protected final Object _instantiationFailure(DeserializationContext ctxt, Throwa // Called from the generated exception handler that covers every property // arm: the stock loop's wrapAndThrow, so property errors carry the - // reference path. Always throws; the return type only satisfies the - // generated athrow. + // reference path. inputName is the name the generated arm read, or null + // for a bean without aliases or case-insensitive matching, where it always + // is the property's own name; resolved here, so the arms pay nothing for + // such beans. Always throws; the return type only satisfies the generated + // athrow. protected final RuntimeException _propertyException(Throwable t, Object beanOrNull, - SettableBeanProperty prop, DeserializationContext ctxt) { + SettableBeanProperty prop, String inputName, DeserializationContext ctxt) { Object ref = (beanOrNull == null) ? handledType() : beanOrNull; - throw wrapAndThrow(t, ref, prop.getName(), ctxt); + throw wrapAndThrow(t, ref, _failedPropertyName(inputName, prop), ctxt); } // A generated store (setter call or field write) threw: what the stock // property throws for it, through the stock loop's wrapAndThrow. protected final RuntimeException _storeFailure(Throwable t, Object bean, Object childValue, SettableBeanProperty prop, JsonParser p, - DeserializationContext ctxt) { + String inputName, DeserializationContext ctxt) { throw wrapAndThrow(ReaderSupport.storeException(t, childValue, prop, p), bean, - prop.getName(), ctxt); + _failedPropertyName(inputName, prop), ctxt); } /* @@ -133,6 +137,23 @@ protected final void _handleUnknown(JsonParser p, DeserializationContext ctxt, handleUnknownVanilla(p, ctxt, bean, name); } + // Non-Jackson exceptions (like ones from a DeserializationProblemHandler) get + // the property as their path; stock lets them through as they are. All routes + // to unknown-property handling come here, including those of stock code the + // codec inherits, and buffered unknown properties of records. + @Override + protected void handleUnknownProperty(JsonParser p, DeserializationContext ctxt, + Object beanOrClass, String propName) { + try { + super.handleUnknownProperty(p, ctxt, beanOrClass, propName); + } catch (JacksonException e) { // has its path already + throw e; + } catch (Exception e) { + throw wrapAndThrow(e, (beanOrClass == null) ? handledType() : beanOrClass, + propName, ctxt); + } + } + // Neither a name nor the end of the object where a name was expected. protected final Object _unexpectedToken(JsonParser p, DeserializationContext ctxt, Object bean) { diff --git a/blackbird/src/main/java25/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java b/blackbird/src/main/java25/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java index 8f817a81a..c3fbffacc 100644 --- a/blackbird/src/main/java25/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java +++ b/blackbird/src/main/java25/tools/jackson/module/blackbird/codegen/BeanReaderGenerator.java @@ -121,19 +121,13 @@ final class BeanReaderGenerator CD_DESER_CONTEXT, CD_SETTABLE_PROP, CD_FORWARD_REF, CD_OBJECT); private static final MethodTypeDesc MTD_CREATION_FAILURE = MethodTypeDesc.of(CD_OBJECT, CD_DESER_CONTEXT, CD_THROWABLE); + // Input name (or null, see loadInputName) comes before the DeserializationContext. private static final MethodTypeDesc MTD_PROP_WRAP = MethodTypeDesc.of( - CD_RTE, CD_THROWABLE, CD_OBJECT, CD_SETTABLE_PROP, CD_DESER_CONTEXT); - private static final MethodTypeDesc MTD_STORE_FAILURE = MethodTypeDesc.of(CD_RTE, - CD_THROWABLE, CD_OBJECT, CD_OBJECT, CD_SETTABLE_PROP, CD_JSON_PARSER, - CD_DESER_CONTEXT); - // Builder codecs: the same with the input name and the entry kind (see - // GeneratedBuilderBase._failedName) before the DeserializationContext. - private static final MethodTypeDesc MTD_BUILDER_PROP_WRAP = MethodTypeDesc.of( CD_RTE, CD_THROWABLE, CD_OBJECT, CD_SETTABLE_PROP, ConstantDescs.CD_String, - ConstantDescs.CD_boolean, CD_DESER_CONTEXT); - private static final MethodTypeDesc MTD_BUILDER_STORE_FAILURE = MethodTypeDesc.of(CD_RTE, + CD_DESER_CONTEXT); + private static final MethodTypeDesc MTD_STORE_FAILURE = MethodTypeDesc.of(CD_RTE, CD_THROWABLE, CD_OBJECT, CD_OBJECT, CD_SETTABLE_PROP, CD_JSON_PARSER, - ConstantDescs.CD_String, ConstantDescs.CD_boolean, CD_DESER_CONTEXT); + ConstantDescs.CD_String, CD_DESER_CONTEXT); private static final MethodTypeDesc MTD_IS_ENABLED = MethodTypeDesc.of(ConstantDescs.CD_boolean, CD_DESER_FEATURE); private static final MethodTypeDesc MTD_GET_ACTIVE_VIEW = @@ -344,18 +338,9 @@ private static void buildDeserialize(CodeBuilder cob, ReaderCodegen.Plan plan) { final int excSlot = 7; final int maskSlot = 8; final int valueSlot = 10; - // Builder mode with names that can differ from the property's own - // (see GeneratedBuilderBase._failedName): each arm reads the input - // name, and the first match records the entry token. - final boolean inputNames = builderMode && plan.inputNames(); - final int inputNameSlot = 11; - final int startObjectSlot = 12; - // Set once a name has gone to unknown-name handling; tracked only - // where stock reads with its vanilla loop. - final boolean trackUnknown = plan.vanillaLoop(); - final int afterUnknownSlot = 13; - final Names names = !builderMode ? null - : inputNames ? new Names(inputNameSlot, startObjectSlot) : Names.NONE; + // Names that can differ from the property's own: each arm reads the + // input name, for its failure handlers. Negative if they cannot. + final int inputNameSlot = plan.inputNames() ? 11 : -1; Label propHandler = cob.newLabel(); Label storeHandler = cob.newLabel(); @@ -413,9 +398,6 @@ private static void buildDeserialize(CodeBuilder cob, ReaderCodegen.Plan plan) { .invokevirtual(base, "_injectValues", MTD_INJECT); } - if (trackUnknown) { - cob.iconst_0().istore(afterUnknownSlot); - } ldcData(cob, "matcher", CD_NAME_MATCHER); cob.astore(matcherSlot); @@ -425,7 +407,7 @@ private static void buildDeserialize(CodeBuilder cob, ReaderCodegen.Plan plan) { Label oddToken = cob.newLabel(); Label defaultCase = cob.newLabel(); - emitFirstMatch(cob, parser, matcherSlot, ixSlot, inputNames ? startObjectSlot : -1); + emitFirstMatch(cob, parser, matcherSlot, ixSlot); Label[] caseLabels = emitLoopHead(cob, plan, ixSlot, loop, endObject, unknown, oddToken, defaultCase); @@ -435,10 +417,8 @@ private static void buildDeserialize(CodeBuilder cob, ReaderCodegen.Plan plan) { emitArmViewCheck(cob, base, parser, ctxt, maskSlot, matcherSlot, ixSlot, i, plan.stockName()[i], loop); } - if (inputNames) { - // As stock's general loop: the name of the value token. - cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentName", MTD_GET_STRING); - cob.astore(inputNameSlot); + if (inputNameSlot >= 0) { + emitReadInputName(cob, parser, inputNameSlot); } Label armEnd = beginArm(cob, plan.stockName()[i], propSlot, propHandler); GenProp prop = props.get(i); @@ -482,41 +462,25 @@ private static void buildDeserialize(CodeBuilder cob, ReaderCodegen.Plan plan) { } cob.labelBinding(unknown); - if (trackUnknown) { - cob.iconst_1().istore(afterUnknownSlot); - } cob.aload(0).aload(parser).aload(ctxt).aload(beanSlot); cob.invokevirtual(base, "_handleUnknown", MTD_HANDLE_UNKNOWN); nextNameMatch(cob, parser, matcherSlot, ixSlot); cob.goto_(loop); cob.labelBinding(oddToken); - if (trackUnknown) { - // Once an unknown name has been seen, stock's vanilla loop - // (_vanillaDeserializeWithUnknown) returns the bean as it is. - Label beforeUnknown = cob.newLabel(); - cob.iload(afterUnknownSlot).ifeq(beforeUnknown); - cob.aload(beanSlot).areturn(); - cob.labelBinding(beforeUnknown); - } cob.aload(0).aload(parser).aload(ctxt).aload(beanSlot) .invokevirtual(base, "_unexpectedToken", MTD_UNEXPECTED) .areturn(); - emitPropertyHandler(cob, base, propHandler, ctxt, propSlot, excSlot, beanSlot, names); + emitPropertyHandler(cob, base, propHandler, ctxt, propSlot, excSlot, beanSlot, inputNameSlot); if (anyStore) { cob.labelBinding(storeHandler); cob.astore(excSlot); cob.aload(0).aload(excSlot).aload(beanSlot).aload(valueSlot).aload(propSlot) .aload(parser); - if (builderMode) { - names.load(cob); - cob.aload(ctxt); - cob.invokevirtual(base, "_storeFailure", MTD_BUILDER_STORE_FAILURE); - } else { - cob.aload(ctxt); - cob.invokevirtual(base, "_storeFailure", MTD_STORE_FAILURE); - } + loadInputName(cob, inputNameSlot); + cob.aload(ctxt); + cob.invokevirtual(base, "_storeFailure", MTD_STORE_FAILURE); cob.athrow(); } if (ctorHandler != null) { @@ -545,15 +509,9 @@ private static void buildDeserialize(CodeBuilder cob, ReaderCodegen.Plan plan) { if (anyStore) { cob.localVariable(valueSlot, "value", CD_OBJECT, scopeStart, scopeEnd); } - if (inputNames) { + if (inputNameSlot >= 0) { cob.localVariable(inputNameSlot, "inputName", ConstantDescs.CD_String, scopeStart, scopeEnd); - cob.localVariable(startObjectSlot, "startObject", ConstantDescs.CD_boolean, - scopeStart, scopeEnd); - } - if (trackUnknown) { - cob.localVariable(afterUnknownSlot, "afterUnknown", ConstantDescs.CD_boolean, - scopeStart, scopeEnd); } } @@ -744,6 +702,8 @@ private static void buildRecordDeserialize(CodeBuilder cob, ReaderCodegen.Plan p final int beanSlot = next++; final int pendingSlot = next++; final int referringsSlot = next++; + // Names that can differ from the component's own: see the POJO loop. + final int inputNameSlot = plan.inputNames() ? next++ : -1; // Primitive components: the stock value when it is not exactly the // wrapper (see emitStockValueToLocal). int[] rawSlot = new int[n]; @@ -789,7 +749,7 @@ private static void buildRecordDeserialize(CodeBuilder cob, ReaderCodegen.Plan p Label oddToken = cob.newLabel(); Label defaultCase = cob.newLabel(); - emitFirstMatch(cob, parser, matcherSlot, ixSlot, -1); + emitFirstMatch(cob, parser, matcherSlot, ixSlot); Label[] caseLabels = emitLoopHead(cob, plan, ixSlot, loop, endObject, unknown, oddToken, defaultCase); @@ -804,6 +764,9 @@ private static void buildRecordDeserialize(CodeBuilder cob, ReaderCodegen.Plan p emitArmViewCheck(cob, base, parser, ctxt, maskSlot, matcherSlot, ixSlot, i, plan.stockName()[i], loop); } + if (inputNameSlot >= 0) { + emitReadInputName(cob, parser, inputNameSlot); + } GenProp prop = props.get(i); // A stock component value may be a forward Object Id reference, // which the stock loop catches; the region registers before the @@ -974,12 +937,14 @@ private static void buildRecordDeserialize(CodeBuilder cob, ReaderCodegen.Plan p nextNameMatch(cob, parser, matcherSlot, ixSlot); cob.goto_(loop); - // Stock's property-based loop runs only while it sees names: any - // other token ends it, and the record builds as at END_OBJECT. + // As stock's property-based loop: the record is only built at END_OBJECT, + // any other token gets reported. cob.labelBinding(oddToken); - cob.goto_(endObject); + cob.aload(0).aload(parser).aload(ctxt).aconst_null() + .invokevirtual(base, "_unexpectedToken", MTD_UNEXPECTED) + .areturn(); - emitPropertyHandler(cob, base, propHandler, ctxt, propSlot, excSlot, -1, null); + emitPropertyHandler(cob, base, propHandler, ctxt, propSlot, excSlot, -1, inputNameSlot); emitDelegateTail(cob, base, parser, ctxt, delegate); Label scopeEnd = cob.newBoundLabel(); @@ -1002,6 +967,10 @@ private static void buildRecordDeserialize(CodeBuilder cob, ReaderCodegen.Plan p cob.localVariable(beanSlot, "bean", CD_OBJECT, scopeStart, scopeEnd); cob.localVariable(pendingSlot, "ctorFailure", CD_THROWABLE, scopeStart, scopeEnd); cob.localVariable(referringsSlot, "referrings", CD_OBJECT, scopeStart, scopeEnd); + if (inputNameSlot >= 0) { + cob.localVariable(inputNameSlot, "inputName", ConstantDescs.CD_String, + scopeStart, scopeEnd); + } if (views == ViewStrategy.MASK) { cob.localVariable(maskSlot, "viewMask", ConstantDescs.CD_long, scopeStart, scopeEnd); } @@ -1182,26 +1151,27 @@ private static Label beginArm(CodeBuilder cob, String stockName, int propSlot, L return armEnd; } - // The builder failure handlers' name arguments: the locals holding the - // input name and the entry kind, or null and false where the names - // cannot differ (NONE). - private record Names(int inputNameSlot, int startObjectSlot) { - static final Names NONE = new Names(-1, -1); + // As stock's loops: the name of the value token, which arrives after + // nextNameMatchAndToken has already moved past the name. + private static void emitReadInputName(CodeBuilder cob, int parser, int inputNameSlot) { + cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentName", MTD_GET_STRING); + cob.astore(inputNameSlot); + } - void load(CodeBuilder cob) { - if (inputNameSlot < 0) { - cob.aconst_null().iconst_0(); - } else { - cob.aload(inputNameSlot).iload(startObjectSlot); - } + // The failure handlers' name argument: the local holding the input name, + // or null where the names cannot differ (negative slot). + private static void loadInputName(CodeBuilder cob, int inputNameSlot) { + if (inputNameSlot < 0) { + cob.aconst_null(); + } else { + cob.aload(inputNameSlot); } } - // receiverSlot < 0 selects aconst_null (record mode: no instance yet); - // non-null names (builder mode) passes the name arguments. + // receiverSlot < 0 selects aconst_null (record mode: no instance yet). private static void emitPropertyHandler(CodeBuilder cob, ClassDesc base, Label handler, int ctxt, - int propSlot, int excSlot, int receiverSlot, Names names) { + int propSlot, int excSlot, int receiverSlot, int inputNameSlot) { cob.labelBinding(handler); cob.astore(excSlot); cob.aload(0); @@ -1212,14 +1182,9 @@ private static void emitPropertyHandler(CodeBuilder cob, ClassDesc base, Label h cob.aload(receiverSlot); } cob.aload(propSlot); - if (names != null) { - names.load(cob); - cob.aload(ctxt); - cob.invokevirtual(base, "_propertyException", MTD_BUILDER_PROP_WRAP); - } else { - cob.aload(ctxt); - cob.invokevirtual(base, "_propertyException", MTD_PROP_WRAP); - } + loadInputName(cob, inputNameSlot); + cob.aload(ctxt); + cob.invokevirtual(base, "_propertyException", MTD_PROP_WRAP); cob.athrow(); } @@ -1238,23 +1203,16 @@ private static void nextNameMatch(CodeBuilder cob, int parser, int matcherSlot, // the current token, so a matched name entry advances to its value here; // fusing the entry itself would double-advance (the current name is // already consumed). Negative results stay on the name, as the unknown - // arm expects. startObjectSlot >= 0 records which entry was taken. - private static void emitFirstMatch(CodeBuilder cob, int parser, int matcherSlot, int ixSlot, - int startObjectSlot) { + // arm expects. + private static void emitFirstMatch(CodeBuilder cob, int parser, int matcherSlot, int ixSlot) { Label nameEntry = cob.newLabel(); Label done = cob.newLabel(); cob.aload(parser).invokevirtual(CD_JSON_PARSER, "currentToken", MTD_NEXT_TOKEN); cob.getstatic(CD_JSON_TOKEN, "PROPERTY_NAME", CD_JSON_TOKEN); cob.if_acmpeq(nameEntry); - if (startObjectSlot >= 0) { - cob.iconst_1().istore(startObjectSlot); - } nextNameMatch(cob, parser, matcherSlot, ixSlot); cob.goto_(done); cob.labelBinding(nameEntry); - if (startObjectSlot >= 0) { - cob.iconst_0().istore(startObjectSlot); - } cob.aload(parser).aload(matcherSlot) .invokevirtual(CD_JSON_PARSER, "currentNameMatch", MTD_NEXT_NAME_MATCH) .istore(ixSlot); diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/codegen/ReaderPlanCheckTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/codegen/ReaderPlanCheckTest.java index 3f4f5871a..2610aa0ce 100644 --- a/blackbird/src/test/java/tools/jackson/module/blackbird/codegen/ReaderPlanCheckTest.java +++ b/blackbird/src/test/java/tools/jackson/module/blackbird/codegen/ReaderPlanCheckTest.java @@ -30,7 +30,7 @@ public void scalarArmWithoutStoreFails() { IllegalStateException e = assertThrows(IllegalStateException.class, () -> ReaderCodegen.generate(Bean.class, List.of(new GenProp("x", kind, null)), null, new int[] { 0 }, null, null, Construction.instantiator(null), - false, false, ViewStrategy.NONE, null)); + false, ViewStrategy.NONE, null)); assertTrue(e.getMessage().contains("no store handle for " + kind), e.getMessage()); } } diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecStockChecksTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecStockChecksTest.java index fb484c36d..dc1e2d2cf 100644 --- a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecStockChecksTest.java +++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBCodecStockChecksTest.java @@ -12,13 +12,16 @@ import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.annotation.ObjectIdGenerators; import tools.jackson.core.JsonParser; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; import tools.jackson.databind.ValueDeserializer; import tools.jackson.databind.annotation.JsonDeserialize; import tools.jackson.databind.deser.DeserializationProblemHandler; @@ -180,6 +183,55 @@ public static class RefDoc { public List items; } + // A POJO property with an alias; a negative value fails in the setter. + public static class AliasedPojo { + int v; + + @JsonAlias("alt") + public void setV(int v) { + if (v < 0) { + throw new IllegalArgumentException("negative"); + } + this.v = v; + } + } + + public record AliasedRecord(@JsonAlias("alt") int v) { } + + public static class AliasedChild { + public int n; + } + + // Aliased properties with structured values: the input name gets read at + // START_OBJECT or START_ARRAY. Fields and setters, child codecs and stock. + public static class AliasedStructured { + @JsonAlias("c1") public AliasedChild child; + @JsonAlias("l1") public List list; + @JsonAlias("f1") public int count; + + @JsonAlias("t1") + public void setThrowing(AliasedChild c) { + throw new IllegalArgumentException("setter failure"); + } + } + + public record AliasedStructuredRecord(@JsonAlias("c1") AliasedChild child, + @JsonAlias("l1") List list) { } + + public static class ViewA { } + public static class ViewB { } + + // Under an active view, with properties that the view hides + public static class AliasedViews { + @JsonView(ViewA.class) @JsonAlias("a1") public int a; + @JsonView(ViewB.class) @JsonAlias("b1") public int b; + @JsonView(ViewA.class) @JsonAlias("c1") public AliasedChild c; + } + + public record AliasedViewsRecord(@JsonView(ViewA.class) @JsonAlias("a1") int a, + @JsonView(ViewB.class) @JsonAlias("b1") int b, + @JsonView(ViewA.class) @JsonAlias("c1") AliasedChild c) { } + private static final ObjectMapper VANILLA = newVanillaJSONMapper(); private static final ObjectMapper MODULE = newObjectMapper(); @@ -298,22 +350,91 @@ public Object handleInstantiationProblem(DeserializationContext ctxt, r -> r.ref().get().name + "/" + r.items().get(0).name + "/" + r.tag()); } + // As builder properties below: POJO properties and record components are + // reported under the name in the input, with default settings and with + // those for stock's vanilla loop. + @Test + public void testPojoAndRecordFailureReportsInputName() throws Exception { + _assertFailureNames(VANILLA, MODULE); + _assertFailureNames( + JsonMapper.builder().enable(MapperFeature.DEFAULT_VIEW_INCLUSION).build(), + blackbirdMapperBuilder().enable(MapperFeature.DEFAULT_VIEW_INCLUSION).build()); + } + + @Test + public void testFailureUnderCaseInsensitiveMapper() throws Exception { + ObjectMapper vanilla = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES).build(); + ObjectMapper module = blackbirdMapperBuilder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES).build(); + _assertSameFailure(vanilla, module, "{\"V\":-1}", AliasedPojo.class, "V"); + _assertSameFailure(vanilla, module, "{\"V\":\"x\"}", AliasedRecord.class, "V"); + } + + private void _assertFailureNames(ObjectMapper vanilla, ObjectMapper module) throws Exception { + assertGeneratedReader(module, AliasedPojo.class); + assertGeneratedReader(module, AliasedRecord.class); + // Scalar read, then setter (the store handler) + _assertSameFailure(vanilla, module, "{\"alt\":\"x\"}", AliasedPojo.class, "alt"); + _assertSameFailure(vanilla, module, "{\"alt\":-1}", AliasedPojo.class, "alt"); + _assertSameFailure(vanilla, module, "{\"v\":-1}", AliasedPojo.class, "v"); + _assertSameFailure(vanilla, module, "{\"alt\":\"x\"}", AliasedRecord.class, "alt"); + _assertSameFailure(vanilla, module, "{\"v\":\"x\"}", AliasedRecord.class, "v"); + } + + @Test + public void testFailureNameWithStructuredValue() throws Exception { + for (Class type : List.of(AliasedStructured.class, AliasedStructuredRecord.class)) { + assertGeneratedReader(MODULE, type); + _assertSameFailure(VANILLA, MODULE, a2q("{'c1':[]}"), type, "c1"); + _assertSameFailure(VANILLA, MODULE, a2q("{'l1':{}}"), type, "l1"); + // Failures within: names within do not replace this name + _assertSameFailure(VANILLA, MODULE, a2q("{'c1':{'n':'x'}}"), type, "c1"); + _assertSameFailure(VANILLA, MODULE, a2q("{'l1':['x']}"), type, "l1"); + _assertSameFailure(VANILLA, MODULE, a2q("{'c1':{'n':1},'l1':{}}"), type, "l1"); + _assertSameFailure(VANILLA, MODULE, a2q("{'child':[]}"), type, "child"); + } + _assertSameFailure(VANILLA, MODULE, a2q("{'f1':'x'}"), AliasedStructured.class, "f1"); + // Setter fails after child has been read + _assertSameFailure(VANILLA, MODULE, a2q("{'t1':{'n':1}}"), AliasedStructured.class, "t1"); + } + + @Test + public void testFailureNameUnderActiveView() throws Exception { + for (Class type : List.of(AliasedViews.class, AliasedViewsRecord.class)) { + assertGeneratedReader(MODULE, type); + final ObjectReader vanilla = VANILLA.readerFor(type).withView(ViewA.class); + final ObjectReader module = MODULE.readerFor(type).withView(ViewA.class); + _assertSameFailure(vanilla, module, a2q("{'a1':'x'}"), "a1"); + _assertSameFailure(vanilla, module, a2q("{'c1':[]}"), "c1"); + // Past a property hidden by the view, with invalid value that gets skipped + _assertSameFailure(vanilla, module, a2q("{'b1':'x','a1':'y'}"), "a1"); + _assertSameFailure(vanilla, module, a2q("{'b1':{'z':1},'c1':[]}"), "c1"); + } + } + + private void _assertSameFailure(ObjectMapper vanilla, ObjectMapper module, + String doc, Class type, String expName) { + _assertSameFailure(vanilla.readerFor(type), module.readerFor(type), doc, expName); + } + + private void _assertSameFailure(ObjectReader vanilla, ObjectReader module, + String doc, String expName) { + Exception expected = assertThrows(Exception.class, () -> vanilla.readValue(doc)); + Exception actual = assertThrows(Exception.class, () -> module.readValue(doc)); + assertEquals(expected.getClass(), actual.getClass(), doc); + assertEquals(expected.getMessage(), actual.getMessage(), doc); + assertTrue(actual.getMessage().contains("[\"" + expName + "\"]"), actual.getMessage()); + } + // A failing builder property is reported under the name in the input, - // as stock's general builder loop does: here the alias. + // as stock does: here the alias. @Test public void testBuilderFailureReportsInputName() throws Exception { assertGeneratedReader(MODULE, Aliased.class); - for (String doc : new String[] { "{\"alt\":\"x\"}", "{\"alt\":-1}", "{\"v\":-1}" }) { - Exception expected = assertThrows(Exception.class, - () -> VANILLA.readValue(doc, Aliased.class)); - Exception actual = assertThrows(Exception.class, - () -> MODULE.readValue(doc, Aliased.class)); - assertEquals(expected.getClass(), actual.getClass(), doc); - assertEquals(expected.getMessage(), actual.getMessage(), doc); - } - Exception e = assertThrows(Exception.class, - () -> MODULE.readValue("{\"alt\":-1}", Aliased.class)); - assertTrue(e.getMessage().contains("[\"alt\"]"), e.getMessage()); + _assertSameFailure(VANILLA, MODULE, "{\"alt\":\"x\"}", Aliased.class, "alt"); + _assertSameFailure(VANILLA, MODULE, "{\"alt\":-1}", Aliased.class, "alt"); + _assertSameFailure(VANILLA, MODULE, "{\"v\":-1}", Aliased.class, "v"); } // The same after a type id that follows a buffered property: the diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBReaderOddTokenTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBReaderOddTokenTest.java index 1813f52e6..25cebed04 100644 --- a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBReaderOddTokenTest.java +++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBReaderOddTokenTest.java @@ -5,10 +5,14 @@ import org.junit.jupiter.api.Test; import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; import tools.jackson.core.ObjectReadContext; +import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.deser.DeserializationProblemHandler; import tools.jackson.databind.exc.MismatchedInputException; import tools.jackson.databind.util.TokenBuffer; import tools.jackson.module.blackbird.BlackbirdTestBase; @@ -17,14 +21,11 @@ /** * A token that is neither a name nor END_OBJECT where a name is expected - * (possible from a non-JSON parser or a replayed buffer). Stock's vanilla - * loop reports it as unexpected, unless an unknown name came before it: then - * _vanillaDeserializeWithUnknown returns the bean as it is. Stock takes the - * vanilla loop only with DEFAULT_VIEW_INCLUSION on (off by default in 3.x); - * otherwise its general loop reports the token either way. A record (stock's - * property-based loop, which runs only while it sees names) ends its loop on - * the token and builds as at END_OBJECT. Every case compares against a - * vanilla mapper. + * (possible from a non-JSON parser or a replayed buffer). Since [databind#6357] + * all of stock's loops report it as unexpected, with or without an unknown name + * before it, and a record does not get built. Stock takes the vanilla loop only + * with DEFAULT_VIEW_INCLUSION on (off by default in 3.x). Every case compares + * against a vanilla mapper. */ public class BBReaderOddTokenTest extends BlackbirdTestBase { @@ -46,14 +47,10 @@ public record RequiredRec(String s, @JsonProperty(required = true) int i) { } .enable(MapperFeature.DEFAULT_VIEW_INCLUSION).build(); @Test - public void oddTokenAfterUnknownReturnsBean() throws Exception + public void oddTokenAfterUnknownFailsWithVanillaLoop() throws Exception { assertGeneratedReader(viewInclusionMapper, Plain.class); - Plain expected = read(viewInclusionVanilla, true); - Plain actual = read(viewInclusionMapper, true); - assertEquals("a", expected.s); - assertEquals("a", actual.s); - assertEquals(expected.i, actual.i); + assertSameFailure(viewInclusionVanilla, viewInclusionMapper, true); } @Test @@ -73,24 +70,38 @@ public void oddTokenWithoutUnknownFails() throws Exception } @Test - public void oddTokenEndsRecord() throws Exception + public void oddTokenInRecordFails() throws Exception { assertGeneratedReader(mapper, Rec.class); for (boolean withUnknown : new boolean[] { false, true }) { - Rec expected = read(vanilla, withUnknown, Rec.class); - assertEquals(new Rec("a", 0), expected); - assertEquals(expected, read(mapper, withUnknown, Rec.class)); + assertSameFailure(vanilla, mapper, withUnknown, Rec.class); } } - // The component after the token is missing, as at END_OBJECT. + // Reported before missing required component would be @Test - public void oddTokenEndsRecordMissingRequired() throws Exception + public void oddTokenInRecordMissingRequiredFails() throws Exception { assertGeneratedReader(mapper, RequiredRec.class); assertSameFailure(vanilla, mapper, false, RequiredRec.class); } + // Problem handler gets to provide the value, in place of building the record + @Test + public void oddTokenInRecordWithProblemHandler() throws Exception + { + final DeserializationProblemHandler handler = new DeserializationProblemHandler() { + @Override + public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, + JsonToken t, JsonParser p, String failureMsg) { + return new Rec("handler", 1); + } + }; + Rec expected = new Rec("handler", 1); + assertEquals(expected, read(vanilla.rebuild().addHandler(handler).build(), false, Rec.class)); + assertEquals(expected, read(mapper.rebuild().addHandler(handler).build(), false, Rec.class)); + } + private static void assertSameFailure(ObjectMapper stock, ObjectMapper bb, boolean withUnknown) { @@ -107,11 +118,6 @@ private static void assertSameFailure(ObjectMapper stock, ObjectMapper bb, assertEquals(expected.getOriginalMessage(), actual.getOriginalMessage()); } - private static Plain read(ObjectMapper m, boolean withUnknown) throws Exception - { - return read(m, withUnknown, Plain.class); - } - // {"s":"a", ["x":1,] 3, "i":5}: a bare number where a name belongs. private static T read(ObjectMapper m, boolean withUnknown, Class type) throws Exception @@ -130,7 +136,7 @@ private static T read(ObjectMapper m, boolean withUnknown, Class type) tb.writeEndObject(); try (JsonParser p = tb.asParser(ObjectReadContext.empty())) { return m.readerFor(type) - // Stock's early return leaves the rest of the object + // A problem handler's value leaves the rest of the object // unread. .without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, DeserializationFeature.FAIL_ON_TRAILING_TOKENS) diff --git a/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBUnknownPropertyFailureTest.java b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBUnknownPropertyFailureTest.java new file mode 100644 index 000000000..1d44dd94f --- /dev/null +++ b/blackbird/src/test/java/tools/jackson/module/blackbird/deser/BBUnknownPropertyFailureTest.java @@ -0,0 +1,127 @@ +package tools.jackson.module.blackbird.deser; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonView; + +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.deser.DeserializationProblemHandler; +import tools.jackson.databind.exc.UnrecognizedPropertyException; +import tools.jackson.module.blackbird.BlackbirdTestBase; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Non-Jackson exception from handling of an unknown property gets the property + * as its path. Unlike most of Blackbird's behavior this is not same as stock, + * which does not wrap these. + */ +public class BBUnknownPropertyFailureTest extends BlackbirdTestBase +{ + public static class Pojo { + public int a; + } + + public record Rec(int a) { } + + @JsonDeserialize(builder = Built.Builder.class) + public static class Built { + public static class Builder { + int a; + public Builder withA(int v) { a = v; return this; } + public Built build() { return new Built(); } + } + } + + public static class ViewA { } + + public static class ViewPojo { + @JsonView(ViewA.class) public int a; + } + + private static final List> TYPES = List.of(Pojo.class, Rec.class, Built.class); + private static final List DOCS = List.of("{'zzz':1}", "{'a':1,'zzz':1}", "{'zzz':1,'a':1}"); + + private final DeserializationProblemHandler HANDLER = new DeserializationProblemHandler() { + @Override + public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p, + ValueDeserializer deser, Object beanOrClass, String propName) { + throw new IllegalStateException("handler failure"); + } + }; + + private final ObjectMapper MAPPER = mapperBuilder().addHandler(HANDLER).build(); + + @Test + public void wrappedWithDefaultSettings() throws Exception { + for (Class type : TYPES) { + assertGeneratedReader(MAPPER, type); + _verifyWrapped(MAPPER.readerFor(type)); + } + } + + // Settings for stock's vanilla loop + @Test + public void wrappedWithVanillaSettings() throws Exception { + final ObjectMapper mapper = mapperBuilder().addHandler(HANDLER) + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION).build(); + for (Class type : TYPES) { + assertGeneratedReader(mapper, type); + _verifyWrapped(mapper.readerFor(type)); + } + } + + @Test + public void wrappedUnderActiveView() throws Exception { + assertGeneratedReader(MAPPER, ViewPojo.class); + _verifyWrapped(MAPPER.readerFor(ViewPojo.class).withView(ViewA.class)); + } + + // Goes through stock code that the codec inherits + @Test + public void wrappedForUpdate() throws Exception { + _verifyWrapped(MAPPER.readerForUpdating(new Pojo())); + } + + @Test + public void notWrappedIfDisabled() throws Exception { + for (Class type : TYPES) { + assertThrows(IllegalStateException.class, () -> MAPPER.readerFor(type) + .without(DeserializationFeature.WRAP_EXCEPTIONS) + .readValue(a2q("{'zzz':1}"))); + } + } + + // Has its path already: must not get the property twice + @Test + public void jacksonExceptionPassedAsIs() throws Exception { + final ObjectMapper mapper = mapperBuilder() + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); + for (Class type : TYPES) { + UnrecognizedPropertyException e = assertThrows(UnrecognizedPropertyException.class, + () -> mapper.readerFor(type).readValue(a2q("{'zzz':1}"))); + assertEquals(1, e.getPath().size(), e.getMessage()); + assertEquals("zzz", e.getPath().get(0).getPropertyName()); + } + } + + private static void _verifyWrapped(ObjectReader r) { + for (String doc : DOCS) { + DatabindException e = assertThrows(DatabindException.class, + () -> r.readValue(a2q(doc)), doc); + assertInstanceOf(IllegalStateException.class, e.getCause(), doc); + assertEquals(1, e.getPath().size(), doc); + assertEquals("zzz", e.getPath().get(0).getPropertyName(), doc); + } + } +} diff --git a/release-notes/VERSION b/release-notes/VERSION index 07209d008..6d8ba34ba 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -43,6 +43,10 @@ Active maintainers: `tools.jackson.module.blackbird`, as the JPMS module does. (contributed by @stevenschlansker) #362: Bump `byte-buddy` dependency from 1.17.8 to 1.18.15 +#368: (afterburner, blackbird) Report property name as found in input (alias or + different case), not canonical name, in deserialization error paths, and wrap + non-Jackson exceptions from handling of unknown properties + (fix by @cowtowncoder, w/ Claude code) 3.2.5 (not yet released)