From ad62c049b5be52a51f01d6a880721b460510bb02 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 21 Oct 2026 11:11:25 -0400 Subject: [PATCH 01/34] Use Blackbird generated setters for builder deserialization --- .../module/blackbird/BaseBenchmark.java | 5 + .../module/blackbird/BeanWithBuilder.java | 41 +++ .../deser/BBDeserializerModifier.java | 41 ++- .../deser/OptimizedSettableBeanProperty.java | 22 +- .../deser/SettableBooleanProperty.java | 33 ++- .../blackbird/deser/SettableIntProperty.java | 31 ++- .../blackbird/deser/SettableLongProperty.java | 31 ++- .../deser/SettableObjectProperty.java | 24 +- .../deser/SettableStringProperty.java | 24 +- .../module/blackbird/deser/TestBuilders.java | 255 ++++++++++++++++++ 10 files changed, 463 insertions(+), 44 deletions(-) create mode 100644 blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BeanWithBuilder.java diff --git a/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BaseBenchmark.java b/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BaseBenchmark.java index 367528639..48fc7657c 100644 --- a/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BaseBenchmark.java +++ b/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BaseBenchmark.java @@ -88,6 +88,11 @@ public BeanWithPropertyConstructor[] constructorArrayDeser() throws Exception { return mapper.readValue(beansBytes, BeanWithPropertyConstructor[].class); } + @Benchmark + public BeanWithBuilder[] builderArrayDeser() throws Exception { + return mapper.readValue(beansBytes, BeanWithBuilder[].class); + } + @Benchmark public void classicMediaItemSer(final Blackhole bh) throws Exception { mediaItemWriter.writeValue(new NopOutputStream(bh), MediaItem.SAMPLE); diff --git a/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BeanWithBuilder.java b/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BeanWithBuilder.java new file mode 100644 index 000000000..862f0c283 --- /dev/null +++ b/blackbird/benchmarks/src/main/java/com/fasterxml/jackson/module/blackbird/BeanWithBuilder.java @@ -0,0 +1,41 @@ +package com.fasterxml.jackson.module.blackbird; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = BeanWithBuilder.Builder.class) +public class BeanWithBuilder extends SomeBean { + private BeanWithBuilder() { } + + public static class Builder { + private final BeanWithBuilder bean = new BeanWithBuilder(); + + public Builder withPropA(int propA) { + bean.setPropA(propA); + return this; + } + + public Builder withPropB(String propB) { + bean.setPropB(propB); + return this; + } + + public Builder withPropC(long propC) { + bean.setPropC(propC); + return this; + } + + public Builder withPropD(SomeBean propD) { + bean.setPropD(propD); + return this; + } + + public Builder withPropE(SomeEnum propE) { + bean.setPropE(propE); + return this; + } + + public BeanWithBuilder build() { + return bean; + } + } +} diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index a0013b45b..07f803de2 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -181,30 +181,51 @@ private void nextProperty(SettableBeanProperty prop, if (type.isPrimitive()) { if (type == Integer.TYPE) { + ObjIntBiFunction thunk = createThunk(lookup, ObjIntBiFunction.class, int.class, setter); newProps.add(new SettableIntProperty(prop, createSetter(lookup, ObjIntConsumer.class, ObjIntBiFunction.class, - INT_TRAMPOLINE, int.class, setter))); + INT_TRAMPOLINE, int.class, setter, thunk), thunk)); } else if (type == Long.TYPE) { + ObjLongBiFunction thunk = createThunk(lookup, ObjLongBiFunction.class, long.class, setter); newProps.add(new SettableLongProperty(prop, createSetter(lookup, ObjLongConsumer.class, ObjLongBiFunction.class, - LONG_TRAMPOLINE, long.class, setter))); + LONG_TRAMPOLINE, long.class, setter, thunk), thunk)); } else if (type == Boolean.TYPE) { + ObjBooleanBiFunction thunk = createThunk(lookup, ObjBooleanBiFunction.class, boolean.class, setter); newProps.add(new SettableBooleanProperty(prop, createSetter(lookup, ObjBooleanConsumer.class, ObjBooleanBiFunction.class, - BOOLEAN_TRAMPOLINE, boolean.class, setter))); + BOOLEAN_TRAMPOLINE, boolean.class, setter, thunk), thunk)); } } else { + BiFunction thunk = createThunk(lookup, BiFunction.class, Object.class, setter); if (type == String.class) { newProps.add(new SettableStringProperty(prop, - createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter))); + createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), thunk)); } else { newProps.add(new SettableObjectProperty(prop, - createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter))); + createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), thunk)); } } } - private T createSetter(Lookup lookup, Class iface, Class thunkType, MethodHandle trampoline, Class valueType, MethodHandle setter) + // Returns null for void setters, which have nothing to return + private T createThunk(Lookup lookup, Class thunkType, Class valueType, MethodHandle setter) + throws Throwable, LambdaConversionException { + if (setter.type().returnType() == void.class) { + return null; + } + return thunkType.cast(LambdaMetafactory.metafactory( + lookup, + "apply", + MethodType.methodType(thunkType), + MethodType.methodType(Object.class, Object.class, valueType), + setter, + setter.type()) + .getTarget().invoke()); + } + + private T createSetter(Lookup lookup, Class iface, Class thunkType, MethodHandle trampoline, Class valueType, MethodHandle setter, + Object builtThunk) throws Throwable, LambdaConversionException { if (setter.type().returnType() == void.class) { return iface.cast(LambdaMetafactory.metafactory( @@ -216,14 +237,6 @@ private T createSetter(Lookup lookup, Class iface, Class thunkType, Me setter.type()) .getTarget().invoke()); } - Object builtThunk = LambdaMetafactory.metafactory( - lookup, - "apply", - MethodType.methodType(thunkType), - MethodType.methodType(Object.class, Object.class, valueType), - setter, - setter.type()) - .getTarget().invoke(); return iface.cast(LambdaMetafactory.metafactory( MethodHandles.lookup(), "accept", diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index e6b065efd..b2d631f7a 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -81,25 +81,15 @@ public abstract void deserializeAndSet(JsonParser jp, DeserializationContext ctx /********************************************************************** */ - /* !!! 19-Feb-2012, tatu: - * - * We do not yet generate code for these methods: it would not be hugely - * difficult to add them, but let's first see if anyone needs them... - * (it is non-trivial, adds code etc, so not without cost). - * - * That is: we'll use Reflection fallback for Builder-based deserialization, - * so it will not be significantly faster. - */ + // Setters that return a value use the generated accessor and keep the + // returned builder; void setters still go through the delegate. @Override public abstract Object deserializeSetAndReturn(JsonParser jp, DeserializationContext ctxt, Object instance) throws IOException; - @Override - public Object setAndReturn(Object instance, Object value) throws IOException { - return delegate.setAndReturn(instance, value); - } + public abstract Object setAndReturn(Object instance, Object value) throws IOException; /* /********************************************************************** @@ -113,7 +103,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { * * @since 2.9 */ - protected void _reportProblem(Object bean, Object value, Throwable e) + protected Object _reportProblem(Object bean, Object value, Throwable e) throws IOException { if ((e instanceof IllegalAccessError) @@ -129,8 +119,8 @@ protected void _reportProblem(Object bean, Object value, Throwable e) Logger.getLogger(getClass().getName()).log(Level.WARNING, msg, e); } } - delegate.set(bean, value); - return; + // delegate is a MethodProperty: same as `set()`, but also returns the builder + return delegate.setAndReturn(bean, value); } if (e instanceof IOException) { throw (IOException) e; diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index a48c45f13..37af56f35 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -5,22 +5,26 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.module.blackbird.deser.BBDeserializerModifier.ObjBooleanBiFunction; final class SettableBooleanProperty extends OptimizedSettableBeanProperty { private static final long serialVersionUID = 1L; private ObjBooleanConsumer _optimizedSetter; + private ObjBooleanBiFunction _optimizedSetterAndReturn; - public SettableBooleanProperty(SettableBeanProperty src, ObjBooleanConsumer optimizedSetter) + public SettableBooleanProperty(SettableBeanProperty src, ObjBooleanConsumer optimizedSetter, + ObjBooleanBiFunction optimizedSetterAndReturn) { super(src); _optimizedSetter = optimizedSetter; + _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableBooleanProperty(del, _optimizedSetter); + return new SettableBooleanProperty(del, _optimizedSetter, _optimizedSetterAndReturn); } /* @@ -54,10 +58,10 @@ public Object deserializeSetAndReturn(JsonParser p, { JsonToken t = p.currentToken(); if (t == JsonToken.VALUE_TRUE) { - return setAndReturn(instance, Boolean.TRUE); + return _setAndReturn(instance, true); } if (t == JsonToken.VALUE_FALSE) { - return setAndReturn(instance, Boolean.FALSE); + return _setAndReturn(instance, false); } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -72,4 +76,25 @@ public void set(Object bean, Object value) throws IOException { _reportProblem(bean, b, e); } } + + @Override + public Object setAndReturn(Object instance, Object value) throws IOException { + if (value == null) { + return delegate.setAndReturn(instance, value); + } + // not optimal (due to boxing), but better than using reflection: + return _setAndReturn(instance, ((Boolean) value).booleanValue()); + } + + private Object _setAndReturn(Object instance, boolean b) throws IOException { + if (_optimizedSetterAndReturn == null) { + return delegate.setAndReturn(instance, b); + } + try { + Object result = _optimizedSetterAndReturn.apply(instance, b); + return (result == null) ? instance : result; + } catch (Throwable e) { + return _reportProblem(instance, b, e); + } + } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 93fb3828f..f6d45af02 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -6,22 +6,26 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.module.blackbird.deser.BBDeserializerModifier.ObjIntBiFunction; final class SettableIntProperty extends OptimizedSettableBeanProperty { private static final long serialVersionUID = 1L; private ObjIntConsumer _optimizedSetter; + private ObjIntBiFunction _optimizedSetterAndReturn; - public SettableIntProperty(SettableBeanProperty src, ObjIntConsumer optimizedSetter) + public SettableIntProperty(SettableBeanProperty src, ObjIntConsumer optimizedSetter, + ObjIntBiFunction optimizedSetterAndReturn) { super(src); _optimizedSetter = optimizedSetter; + _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableIntProperty(del, _optimizedSetter); + return new SettableIntProperty(del, _optimizedSetter, _optimizedSetterAndReturn); } /* @@ -52,7 +56,7 @@ public Object deserializeSetAndReturn(JsonParser p, throws IOException { if (p.isExpectedNumberIntToken()) { - return setAndReturn(instance, p.getIntValue()); + return _setAndReturn(instance, p.getIntValue()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -67,4 +71,25 @@ public void set(Object bean, Object value) throws IOException { _reportProblem(bean, v, e); } } + + @Override + public Object setAndReturn(Object instance, Object value) throws IOException { + if (value == null) { + return delegate.setAndReturn(instance, value); + } + // not optimal (due to boxing), but better than using reflection: + return _setAndReturn(instance, ((Number) value).intValue()); + } + + private Object _setAndReturn(Object instance, int v) throws IOException { + if (_optimizedSetterAndReturn == null) { + return delegate.setAndReturn(instance, v); + } + try { + Object result = _optimizedSetterAndReturn.apply(instance, v); + return (result == null) ? instance : result; + } catch (Throwable e) { + return _reportProblem(instance, v, e); + } + } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 8f3238fa6..44de2caab 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -6,22 +6,26 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.module.blackbird.deser.BBDeserializerModifier.ObjLongBiFunction; final class SettableLongProperty extends OptimizedSettableBeanProperty { private static final long serialVersionUID = 1L; private ObjLongConsumer _optimizedSetter; + private ObjLongBiFunction _optimizedSetterAndReturn; - public SettableLongProperty(SettableBeanProperty src, ObjLongConsumer optimizedSetter) + public SettableLongProperty(SettableBeanProperty src, ObjLongConsumer optimizedSetter, + ObjLongBiFunction optimizedSetterAndReturn) { super(src); _optimizedSetter = optimizedSetter; + _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableLongProperty(del, _optimizedSetter); + return new SettableLongProperty(del, _optimizedSetter, _optimizedSetterAndReturn); } /* @@ -51,7 +55,7 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { if (p.isExpectedNumberIntToken()) { - return setAndReturn(instance, p.getLongValue()); + return _setAndReturn(instance, p.getLongValue()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -66,4 +70,25 @@ public void set(Object bean, Object value) throws IOException { _reportProblem(bean, v, e); } } + + @Override + public Object setAndReturn(Object instance, Object value) throws IOException { + if (value == null) { + return delegate.setAndReturn(instance, value); + } + // not optimal (due to boxing), but better than using reflection: + return _setAndReturn(instance, ((Number) value).longValue()); + } + + private Object _setAndReturn(Object instance, long v) throws IOException { + if (_optimizedSetterAndReturn == null) { + return delegate.setAndReturn(instance, v); + } + try { + Object result = _optimizedSetterAndReturn.apply(instance, v); + return (result == null) ? instance : result; + } catch (Throwable e) { + return _reportProblem(instance, v, e); + } + } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index a1fb37f75..1006b8a61 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; @@ -12,16 +13,19 @@ final class SettableObjectProperty { private static final long serialVersionUID = 1L; private BiConsumer _optimizedSetter; + private BiFunction _optimizedSetterAndReturn; - public SettableObjectProperty(SettableBeanProperty src, BiConsumer optimizedSetter) + public SettableObjectProperty(SettableBeanProperty src, BiConsumer optimizedSetter, + BiFunction optimizedSetterAndReturn) { super(src); this._optimizedSetter = optimizedSetter; + this._optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableObjectProperty(del, _optimizedSetter); + return new SettableObjectProperty(del, _optimizedSetter, _optimizedSetterAndReturn); } /* @@ -88,4 +92,20 @@ public void set(Object bean, Object v) throws IOException { _reportProblem(bean, v, e); } } + + @Override + public Object setAndReturn(Object instance, Object value) throws IOException { + if (_optimizedSetterAndReturn == null) { + return delegate.setAndReturn(instance, value); + } + if ((value == null) && _skipNulls) { + return instance; + } + try { + Object result = _optimizedSetterAndReturn.apply(instance, value); + return (result == null) ? instance : result; + } catch (Throwable e) { + return _reportProblem(instance, value, e); + } + } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 200893083..d6a503262 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; @@ -12,16 +13,19 @@ final class SettableStringProperty { private static final long serialVersionUID = 1L; private final BiConsumer _optimizedSetter; + private final BiFunction _optimizedSetterAndReturn; - public SettableStringProperty(SettableBeanProperty src, BiConsumer optimizedSetter) + public SettableStringProperty(SettableBeanProperty src, BiConsumer optimizedSetter, + BiFunction optimizedSetterAndReturn) { super(src); _optimizedSetter = optimizedSetter; + _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableStringProperty(del, _optimizedSetter); + return new SettableStringProperty(del, _optimizedSetter, _optimizedSetterAndReturn); } /* @@ -58,4 +62,20 @@ public void set(Object bean, Object value) throws IOException { _reportProblem(bean, value, e); } } + + @Override + public Object setAndReturn(Object instance, Object value) throws IOException { + if (_optimizedSetterAndReturn == null) { + return delegate.setAndReturn(instance, value); + } + if ((value == null) && _skipNulls) { + return instance; + } + try { + Object result = _optimizedSetterAndReturn.apply(instance, value); + return (result == null) ? instance : result; + } catch (Throwable e) { + return _reportProblem(instance, value, e); + } + } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 99b11d26f..10be248ef 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -1,9 +1,13 @@ package com.fasterxml.jackson.module.blackbird.deser; +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.Test; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.impl.MethodProperty; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; import static org.junit.jupiter.api.Assertions.*; @@ -34,6 +38,206 @@ static class Thing { } } + // Builder setters should go through generated accessors, not reflection + static final class MutableBuilder + { + String str; + List list; + int i; + long l; + boolean b; + int reflectiveCalls; + + public MutableBuilder withStr(String v) { + reflectiveCalls += reflectiveCall(); + str = v; + return this; + } + + public MutableBuilder withList(List v) { + reflectiveCalls += reflectiveCall(); + list = v; + return this; + } + + public MutableBuilder withI(int v) { + reflectiveCalls += reflectiveCall(); + i = v; + return this; + } + + public MutableBuilder withL(long v) { + reflectiveCalls += reflectiveCall(); + l = v; + return this; + } + + public MutableBuilder withB(boolean v) { + reflectiveCalls += reflectiveCall(); + b = v; + return this; + } + + public MutableBuilt build() { + return new MutableBuilt(str, list, i, l, b, reflectiveCalls); + } + } + + @JsonDeserialize(builder=MutableBuilder.class) + static class MutableBuilt extends Values { + MutableBuilt(String str, List list, int i, long l, boolean b, int reflectiveCalls) { + super(str, list, i, l, b, reflectiveCalls); + } + } + + // Each setter returns a new builder, so the returned value must be used + static final class CopyingBuilder + { + final String str; + final List list; + final int i; + final long l; + final boolean b; + final int reflectiveCalls; + + public CopyingBuilder() { + this(null, null, 0, 0L, false, 0); + } + + CopyingBuilder(String str, List list, int i, long l, boolean b, int reflectiveCalls) { + this.str = str; + this.list = list; + this.i = i; + this.l = l; + this.b = b; + this.reflectiveCalls = reflectiveCalls; + } + + public CopyingBuilder withStr(String v) { + return new CopyingBuilder(v, list, i, l, b, reflectiveCalls + reflectiveCall()); + } + + public CopyingBuilder withList(List v) { + return new CopyingBuilder(str, v, i, l, b, reflectiveCalls + reflectiveCall()); + } + + public CopyingBuilder withI(int v) { + return new CopyingBuilder(str, list, v, l, b, reflectiveCalls + reflectiveCall()); + } + + public CopyingBuilder withL(long v) { + return new CopyingBuilder(str, list, i, v, b, reflectiveCalls + reflectiveCall()); + } + + public CopyingBuilder withB(boolean v) { + return new CopyingBuilder(str, list, i, l, v, reflectiveCalls + reflectiveCall()); + } + + public CopyingBuilt build() { + return new CopyingBuilt(str, list, i, l, b, reflectiveCalls); + } + } + + @JsonDeserialize(builder=CopyingBuilder.class) + static class CopyingBuilt extends Values { + CopyingBuilt(String str, List list, int i, long l, boolean b, int reflectiveCalls) { + super(str, list, i, l, b, reflectiveCalls); + } + } + + // Stock databind keeps the current builder when a setter returns null + static final class NullReturningBuilder + { + String str; + int i; + + public NullReturningBuilder withStr(String v) { + str = v; + return null; + } + + public NullReturningBuilder withI(int v) { + i = v; + return null; + } + + public NullReturningBuilt build() { + return new NullReturningBuilt(str, i); + } + } + + @JsonDeserialize(builder=NullReturningBuilder.class) + static class NullReturningBuilt extends Values { + NullReturningBuilt(String str, int i) { + super(str, null, i, 0L, false, 0); + } + } + + // Generated accessor fails with an access error, so Blackbird falls back to + // reflection, which must still return the new builder + static final class AccessErrorBuilder + { + final String str; + + public AccessErrorBuilder() { + this(null); + } + + AccessErrorBuilder(String str) { + this.str = str; + } + + public AccessErrorBuilder withStr(String v) { + if (reflectiveCall() == 0) { + throw new IllegalAccessError("boom!"); + } + return new AccessErrorBuilder(v); + } + + public AccessErrorBuilt build() { + return new AccessErrorBuilt(str); + } + } + + @JsonDeserialize(builder=AccessErrorBuilder.class) + static class AccessErrorBuilt { + final String str; + + AccessErrorBuilt(String str) { + this.str = str; + } + } + + static class Values { + final String str; + final List list; + final int i; + final long l; + final boolean b; + final int reflectiveCalls; + + Values(String str, List list, int i, long l, boolean b, int reflectiveCalls) { + this.str = str; + this.list = list; + this.i = i; + this.l = l; + this.b = b; + this.reflectiveCalls = reflectiveCalls; + } + } + + static int reflectiveCall() { + for (StackTraceElement elem : new Throwable().getStackTrace()) { + if (elem.getClassName().equals(MethodProperty.class.getName())) { + return 1; + } + } + return 0; + } + + private static final String VALUES_JSON = a2q( + "{'str':'foo','list':['a','b'],'i':123456,'l':12345678901,'b':true}"); + /* /********************************************************** /* Test methods, valid cases, non-deferred, no-mixins @@ -53,4 +257,55 @@ public void testSimpleBuilder() throws Exception assertEquals(expected.foo, actual.foo); } + @Test + public void testMutableBuilderSkipsReflection() throws Exception + { + Values v = MAPPER.readValue(VALUES_JSON, MutableBuilt.class); + _verifyValues(v); + assertEquals(0, v.reflectiveCalls); + } + + @Test + public void testCopyingBuilderSkipsReflection() throws Exception + { + Values v = MAPPER.readValue(VALUES_JSON, CopyingBuilt.class); + _verifyValues(v); + assertEquals(0, v.reflectiveCalls); + } + + // Sanity check that the reflection probe works at all + @Test + public void testVanillaBuilderUsesReflection() throws Exception + { + ObjectMapper vanilla = newVanillaJSONMapper(); + Values v = vanilla.readValue(VALUES_JSON, MutableBuilt.class); + _verifyValues(v); + assertEquals(5, v.reflectiveCalls); + v = vanilla.readValue(VALUES_JSON, CopyingBuilt.class); + _verifyValues(v); + assertEquals(5, v.reflectiveCalls); + } + + @Test + public void testNullReturningBuilder() throws Exception + { + Values v = MAPPER.readValue(a2q("{'str':'foo','i':42}"), NullReturningBuilt.class); + assertEquals("foo", v.str); + assertEquals(42, v.i); + } + + @Test + public void testAccessErrorFallbackKeepsReturnedBuilder() throws Exception + { + AccessErrorBuilt v = MAPPER.readValue(a2q("{'str':'foo'}"), AccessErrorBuilt.class); + assertEquals("foo", v.str); + } + + private void _verifyValues(Values v) { + assertEquals("foo", v.str); + assertEquals(Arrays.asList("a", "b"), v.list); + assertEquals(123456, v.i); + assertEquals(12345678901L, v.l); + assertTrue(v.b); + } } From 10d1b110c53a52aba4641b4fc2c8b7c5044a475d Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 21 Oct 2026 12:27:48 -0400 Subject: [PATCH 02/34] Add void-setter and skip-nulls builder tests --- .../module/blackbird/deser/TestBuilders.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 10be248ef..9dc9553ec 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.deser.impl.MethodProperty; @@ -208,6 +210,63 @@ static class AccessErrorBuilt { } } + // Void setters keep going through the delegate + static final class VoidSetterBuilder + { + String str; + List list; + int i; + long l; + boolean b; + + public void withStr(String v) { str = v; } + public void withList(List v) { list = v; } + public void withI(int v) { i = v; } + public void withL(long v) { l = v; } + public void withB(boolean v) { b = v; } + + public VoidSetterBuilt build() { + return new VoidSetterBuilt(str, list, i, l, b); + } + } + + @JsonDeserialize(builder=VoidSetterBuilder.class) + static class VoidSetterBuilt extends Values { + VoidSetterBuilt(String str, List list, int i, long l, boolean b) { + super(str, list, i, l, b, 0); + } + } + + // Explicit nulls must be skipped, leaving the builder's defaults intact + static final class SkipNullsBuilder + { + String str = "default"; + List list = Arrays.asList("keep"); + + @JsonSetter(nulls=Nulls.SKIP) + public SkipNullsBuilder withStr(String v) { + str = v; + return this; + } + + @JsonSetter(nulls=Nulls.SKIP) + public SkipNullsBuilder withList(List v) { + list = v; + return this; + } + + public SkipNullsBuilt build() { + return new SkipNullsBuilt(str, list); + } + } + + @JsonDeserialize(builder=SkipNullsBuilder.class) + static class SkipNullsBuilt extends Values { + SkipNullsBuilt(String str, List list) { + super(str, list, 0, 0L, false, 0); + } + } + static class Values { final String str; final List list; @@ -301,6 +360,20 @@ public void testAccessErrorFallbackKeepsReturnedBuilder() throws Exception assertEquals("foo", v.str); } + @Test + public void testVoidSetterBuilder() throws Exception + { + _verifyValues(MAPPER.readValue(VALUES_JSON, VoidSetterBuilt.class)); + } + + @Test + public void testSkipNullsBuilder() throws Exception + { + Values v = MAPPER.readValue(a2q("{'str':null,'list':null}"), SkipNullsBuilt.class); + assertEquals("default", v.str); + assertEquals(Arrays.asList("keep"), v.list); + } + private void _verifyValues(Values v) { assertEquals("foo", v.str); assertEquals(Arrays.asList("a", "b"), v.list); From 5b493d8637db5fe17677470d5d66b45344b0544c Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Mon, 26 Oct 2026 12:49:34 -0400 Subject: [PATCH 03/34] Test that builder setter failures match stock --- .../module/blackbird/deser/TestBuilders.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 9dc9553ec..c48128f2c 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.deser.impl.MethodProperty; @@ -267,6 +268,25 @@ static class SkipNullsBuilt extends Values { } } + // A failing setter should be reported the same way with and without Blackbird + static final class ThrowingBuilder + { + public ThrowingBuilder withStr(String v) { + throw new IllegalStateException("bad str"); + } + + public ThrowingBuilder withI(int v) { + throw new IllegalStateException("bad i"); + } + + public ThrowingBuilt build() { + return new ThrowingBuilt(); + } + } + + @JsonDeserialize(builder=ThrowingBuilder.class) + static class ThrowingBuilt { } + static class Values { final String str; final List list; @@ -374,6 +394,25 @@ public void testSkipNullsBuilder() throws Exception assertEquals(Arrays.asList("keep"), v.list); } + @Test + public void testThrowingSetterMatchesVanilla() throws Exception + { + for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'i':42}") }) { + JsonMappingException expected = assertThrows(JsonMappingException.class, + () -> newVanillaJSONMapper().readValue(json, ThrowingBuilt.class)); + JsonMappingException actual = assertThrows(JsonMappingException.class, + () -> MAPPER.readValue(json, ThrowingBuilt.class)); + assertEquals(expected.getClass(), actual.getClass()); + assertEquals(expected.getOriginalMessage(), actual.getOriginalMessage()); + assertEquals(expected.getPathReference(), actual.getPathReference()); + assertEquals(expected.getCause().getClass(), actual.getCause().getClass()); + assertEquals(expected.getCause().getMessage(), actual.getCause().getMessage()); + // and the Blackbird failure came from the generated accessor + assertTrue(Arrays.stream(actual.getCause().getStackTrace()) + .noneMatch(e -> e.getClassName().equals(MethodProperty.class.getName()))); + } + } + private void _verifyValues(Values v) { assertEquals("foo", v.str); assertEquals(Arrays.asList("a", "b"), v.list); From 86e17c981f96989e49d36f376f2e79a1065fe6e9 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Mon, 26 Oct 2026 13:15:30 -0400 Subject: [PATCH 04/34] Match stock exception handling for builder setters --- .../deser/OptimizedSettableBeanProperty.java | 72 +++++-- .../deser/SettableBooleanProperty.java | 2 +- .../blackbird/deser/SettableIntProperty.java | 2 +- .../blackbird/deser/SettableLongProperty.java | 2 +- .../deser/SettableObjectProperty.java | 2 +- .../deser/SettableStringProperty.java | 2 +- .../module/blackbird/deser/TestBuilders.java | 177 ++++++++++++------ 7 files changed, 184 insertions(+), 75 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index b2d631f7a..eb18b6157 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.module.blackbird.deser; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.util.logging.Level; import java.util.logging.Logger; @@ -9,6 +10,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -103,24 +105,14 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, * * @since 2.9 */ - protected Object _reportProblem(Object bean, Object value, Throwable e) + protected void _reportProblem(Object bean, Object value, Throwable e) throws IOException { if ((e instanceof IllegalAccessError) || (e instanceof SecurityException)) { - synchronized (this) { - // yes, double-locking, so not guaranteed; but all we want here is to reduce likelihood - // of multiple logging of same underlying problem. Not guaranteed, just improved. - if (!broken) { - broken = true; - String msg = String.format("Disabling Blackbird deserialization for %s (field %s; mutator %s), due to access error (type %s, message=%s)%n", - bean.getClass(), _propName, getClass().getName(), - e.getClass().getName(), e.getMessage()); - Logger.getLogger(getClass().getName()).log(Level.WARNING, msg, e); - } - } - // delegate is a MethodProperty: same as `set()`, but also returns the builder - return delegate.setAndReturn(bean, value); + _logAccessProblem(bean, e); + delegate.set(bean, value); + return; } if (e instanceof IOException) { throw (IOException) e; @@ -135,6 +127,58 @@ protected Object _reportProblem(Object bean, Object value, Throwable e) throw new RuntimeException(e); } + /** + * Like {@link #_reportProblem}, for {@link #setAndReturn}: an access failure of + * the generated accessor falls back to the delegate, and anything the setter + * throws is reported the way {@code MethodProperty.setAndReturn} reports it. + */ + protected Object _reportProblemAndReturn(Object bean, Object value, Throwable e) + throws IOException + { + if (((e instanceof IllegalAccessError) || (e instanceof SecurityException)) + && !_thrownBySetter(e)) { + _logAccessProblem(bean, e); + return delegate.setAndReturn(bean, value); + } + // `Method.invoke` wraps whatever the setter throws, and `MethodProperty` + // translates that wrapper; doing the same keeps type, message, cause and path + _throwAsIOE(new InvocationTargetException(e), value); + return null; // never gets here + } + + // The setter may already have run, so it must not be called again; an + // access failure happens before it is entered. Without a stack trace we + // cannot tell, so assume the setter ran. + private boolean _thrownBySetter(Throwable e) { + StackTraceElement[] trace = e.getStackTrace(); + if (trace.length == 0) { + return true; + } + AnnotatedMember setter = getMember(); + String className = setter.getDeclaringClass().getName(); + String methodName = setter.getName(); + for (StackTraceElement elem : trace) { + if (methodName.equals(elem.getMethodName()) && className.equals(elem.getClassName())) { + return true; + } + } + return false; + } + + private void _logAccessProblem(Object bean, Throwable e) { + synchronized (this) { + // yes, double-locking, so not guaranteed; but all we want here is to reduce likelihood + // of multiple logging of same underlying problem. Not guaranteed, just improved. + if (!broken) { + broken = true; + String msg = String.format("Disabling Blackbird deserialization for %s (field %s; mutator %s), due to access error (type %s, message=%s)%n", + bean.getClass(), _propName, getClass().getName(), + e.getClass().getName(), e.getMessage()); + Logger.getLogger(getClass().getName()).log(Level.WARNING, msg, e); + } + } + } + /* /********************************************************************** /* Helper methods diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 37af56f35..5cfdbe3fe 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -94,7 +94,7 @@ private Object _setAndReturn(Object instance, boolean b) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, b); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblem(instance, b, e); + return _reportProblemAndReturn(instance, b, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index f6d45af02..11eaf6143 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -89,7 +89,7 @@ private Object _setAndReturn(Object instance, int v) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblem(instance, v, e); + return _reportProblemAndReturn(instance, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 44de2caab..0c2a405cc 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -88,7 +88,7 @@ private Object _setAndReturn(Object instance, long v) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblem(instance, v, e); + return _reportProblemAndReturn(instance, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 1006b8a61..54a8e92a3 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -105,7 +105,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblem(instance, value, e); + return _reportProblemAndReturn(instance, value, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index d6a503262..24f144bfa 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -75,7 +75,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblem(instance, value, e); + return _reportProblemAndReturn(instance, value, e); } } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index c48128f2c..b1ddccb06 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -1,16 +1,22 @@ package com.fasterxml.jackson.module.blackbird.deser; +import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.deser.impl.MethodProperty; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; import static org.junit.jupiter.api.Assertions.*; @@ -176,41 +182,6 @@ static class NullReturningBuilt extends Values { } } - // Generated accessor fails with an access error, so Blackbird falls back to - // reflection, which must still return the new builder - static final class AccessErrorBuilder - { - final String str; - - public AccessErrorBuilder() { - this(null); - } - - AccessErrorBuilder(String str) { - this.str = str; - } - - public AccessErrorBuilder withStr(String v) { - if (reflectiveCall() == 0) { - throw new IllegalAccessError("boom!"); - } - return new AccessErrorBuilder(v); - } - - public AccessErrorBuilt build() { - return new AccessErrorBuilt(str); - } - } - - @JsonDeserialize(builder=AccessErrorBuilder.class) - static class AccessErrorBuilt { - final String str; - - AccessErrorBuilt(String str) { - this.str = str; - } - } - // Void setters keep going through the delegate static final class VoidSetterBuilder { @@ -268,15 +239,28 @@ static class SkipNullsBuilt extends Values { } } - // A failing setter should be reported the same way with and without Blackbird + // A failing setter should be reported the same way with and without Blackbird. + // The throwable is created inside the setter, as a real one would be. + static Supplier toThrow; + static int setterCalls; + static final class ThrowingBuilder { - public ThrowingBuilder withStr(String v) { - throw new IllegalStateException("bad str"); + public ThrowingBuilder withStr(String v) throws Exception { + return fail(); } - public ThrowingBuilder withI(int v) { - throw new IllegalStateException("bad i"); + public ThrowingBuilder withI(int v) throws Exception { + return fail(); + } + + private static ThrowingBuilder fail() throws Exception { + setterCalls++; + Throwable t = toThrow.get(); + if (t instanceof Error) { + throw (Error) t; + } + throw (Exception) t; } public ThrowingBuilt build() { @@ -287,6 +271,29 @@ public ThrowingBuilt build() { @JsonDeserialize(builder=ThrowingBuilder.class) static class ThrowingBuilt { } + // Fails inside the setter on the first call only, so a retry would succeed + static int flakyCalls; + + static final class FlakyBuilder + { + String str; + + public FlakyBuilder withStr(String v) { + if (flakyCalls++ == 0) { + throw new SecurityException("thrown by setter"); + } + str = v; + return this; + } + + public FlakyBuilt build() { + return new FlakyBuilt(); + } + } + + @JsonDeserialize(builder=FlakyBuilder.class) + static class FlakyBuilt { } + static class Values { final String str; final List list; @@ -373,11 +380,32 @@ public void testNullReturningBuilder() throws Exception assertEquals(42, v.i); } + // An access failure of the generated accessor happens before the setter + // runs; the reflective fallback must still return the new builder @Test - public void testAccessErrorFallbackKeepsReturnedBuilder() throws Exception + public void testAccessFailureFallsBackToReflection() throws Exception { - AccessErrorBuilt v = MAPPER.readValue(a2q("{'str':'foo'}"), AccessErrorBuilt.class); - assertEquals("foo", v.str); + ObjectMapper vanilla = newVanillaJSONMapper(); + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) vanilla.getDeserializationContext()) + .createInstance(vanilla.getDeserializationConfig(), null, null); + BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( + vanilla.constructType(CopyingBuilt.class)); + SettableStringProperty prop = new SettableStringProperty(deser.findProperty("str"), + (bean, v) -> { throw new IllegalAccessError("simulated"); }, + (bean, v) -> { throw new IllegalAccessError("simulated"); }); + + CopyingBuilder result = (CopyingBuilder) prop.setAndReturn(new CopyingBuilder(), "foo"); + assertEquals("foo", result.str); + assertEquals(1, result.reflectiveCalls); + } + + @Test + public void testSetterSecurityExceptionIsNotRetried() throws Exception + { + flakyCalls = 0; + assertThrows(JsonMappingException.class, + () -> MAPPER.readValue(a2q("{'str':'foo'}"), FlakyBuilt.class)); + assertEquals(1, flakyCalls); } @Test @@ -397,19 +425,56 @@ public void testSkipNullsBuilder() throws Exception @Test public void testThrowingSetterMatchesVanilla() throws Exception { - for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'i':42}") }) { - JsonMappingException expected = assertThrows(JsonMappingException.class, - () -> newVanillaJSONMapper().readValue(json, ThrowingBuilt.class)); - JsonMappingException actual = assertThrows(JsonMappingException.class, - () -> MAPPER.readValue(json, ThrowingBuilt.class)); - assertEquals(expected.getClass(), actual.getClass()); - assertEquals(expected.getOriginalMessage(), actual.getOriginalMessage()); - assertEquals(expected.getPathReference(), actual.getPathReference()); - assertEquals(expected.getCause().getClass(), actual.getCause().getClass()); - assertEquals(expected.getCause().getMessage(), actual.getCause().getMessage()); - // and the Blackbird failure came from the generated accessor - assertTrue(Arrays.stream(actual.getCause().getStackTrace()) - .noneMatch(e -> e.getClassName().equals(MethodProperty.class.getName()))); + List> failures = Arrays.asList( + () -> new IllegalStateException("unchecked"), + () -> new IllegalStateException("outer", new IllegalArgumentException("root")), + () -> new IOException("bad value"), + () -> new Exception("checked"), + () -> new AssertionError("error"), + () -> new SecurityException("thrown by setter")); + for (boolean wrap : new boolean[] { true, false }) { + ObjectMapper vanilla = JsonMapper.builder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + ObjectMapper blackbird = mapperBuilder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + for (Supplier failure : failures) { + for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'i':42}") }) { + toThrow = failure; + setterCalls = 0; + Throwable expected = assertThrows(Throwable.class, + () -> vanilla.readValue(json, ThrowingBuilt.class)); + Throwable actual = assertThrows(Throwable.class, + () -> blackbird.readValue(json, ThrowingBuilt.class)); + String desc = "wrap=" + wrap + ", " + json + ", " + expected; + assertEquals(2, setterCalls, desc); + _assertSameFailure(desc, expected, actual); + // and the Blackbird failure came from the generated accessor + Throwable root = actual; + while (root.getCause() != null) { + root = root.getCause(); + } + assertTrue(Arrays.stream(root.getStackTrace()) + .noneMatch(e -> e.getClassName().equals(MethodProperty.class.getName())), desc); + } + } + } + } + + private static void _assertSameFailure(String desc, Throwable expected, Throwable actual) { + assertEquals(expected.getClass(), actual.getClass(), desc); + if (expected instanceof JsonMappingException) { + JsonMappingException e = (JsonMappingException) expected; + JsonMappingException a = (JsonMappingException) actual; + assertEquals(e.getOriginalMessage(), a.getOriginalMessage(), desc); + assertEquals(e.getPathReference(), a.getPathReference(), desc); + } else { + assertEquals(expected.getMessage(), actual.getMessage(), desc); + } + Throwable ec = expected.getCause(); + Throwable ac = actual.getCause(); + assertEquals((ec == null) ? null : ec.getClass(), (ac == null) ? null : ac.getClass(), desc); + if (ec != null) { + assertEquals(ec.getMessage(), ac.getMessage(), desc); } } From 066b09ece30ac985f78e5d3fbfb9d1be0d607fa6 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Mon, 26 Oct 2026 13:39:48 -0400 Subject: [PATCH 05/34] Only retry builder setters on accessor access failures --- .../deser/OptimizedSettableBeanProperty.java | 32 +++++----- .../deser/SettableBooleanProperty.java | 2 +- .../blackbird/deser/SettableIntProperty.java | 2 +- .../blackbird/deser/SettableLongProperty.java | 2 +- .../deser/SettableObjectProperty.java | 2 +- .../deser/SettableStringProperty.java | 2 +- .../module/blackbird/deser/TestBuilders.java | 62 ++++++++++++++----- 7 files changed, 67 insertions(+), 37 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index eb18b6157..be5d79b77 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; -import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -132,11 +131,10 @@ protected void _reportProblem(Object bean, Object value, Throwable e) * the generated accessor falls back to the delegate, and anything the setter * throws is reported the way {@code MethodProperty.setAndReturn} reports it. */ - protected Object _reportProblemAndReturn(Object bean, Object value, Throwable e) + protected Object _reportProblemAndReturn(Object accessor, Object bean, Object value, Throwable e) throws IOException { - if (((e instanceof IllegalAccessError) || (e instanceof SecurityException)) - && !_thrownBySetter(e)) { + if (_isAccessorAccessFailure(accessor, e)) { _logAccessProblem(bean, e); return delegate.setAndReturn(bean, value); } @@ -146,23 +144,21 @@ protected Object _reportProblemAndReturn(Object bean, Object value, Throwable e) return null; // never gets here } - // The setter may already have run, so it must not be called again; an - // access failure happens before it is entered. Without a stack trace we - // cannot tell, so assume the setter ran. - private boolean _thrownBySetter(Throwable e) { + // Retrying is only safe if the setter never ran, so require positive evidence: + // the JVM raises an access failure in the accessor's own frame, which is then + // on top of the stack (or, where generated frames are hidden from stack traces, + // this property's frame that called it). A throwable created anywhere else, + // such as one the setter saved and throws later, keeps its original failure. + private boolean _isAccessorAccessFailure(Object accessor, Throwable e) { + if (!(e instanceof IllegalAccessError) && !(e instanceof SecurityException)) { + return false; + } StackTraceElement[] trace = e.getStackTrace(); if (trace.length == 0) { - return true; - } - AnnotatedMember setter = getMember(); - String className = setter.getDeclaringClass().getName(); - String methodName = setter.getName(); - for (StackTraceElement elem : trace) { - if (methodName.equals(elem.getMethodName()) && className.equals(elem.getClassName())) { - return true; - } + return false; } - return false; + String top = trace[0].getClassName(); + return top.equals(accessor.getClass().getName()) || top.equals(getClass().getName()); } private void _logAccessProblem(Object bean, Throwable e) { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 5cfdbe3fe..196ae6ca1 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -94,7 +94,7 @@ private Object _setAndReturn(Object instance, boolean b) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, b); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(instance, b, e); + return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, b, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 11eaf6143..ba5678688 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -89,7 +89,7 @@ private Object _setAndReturn(Object instance, int v) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(instance, v, e); + return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 0c2a405cc..a800b4e45 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -88,7 +88,7 @@ private Object _setAndReturn(Object instance, long v) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(instance, v, e); + return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 54a8e92a3..ccb5dd7de 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -105,7 +105,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(instance, value, e); + return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, value, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 24f144bfa..9958f26f8 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -75,7 +75,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(instance, value, e); + return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, value, e); } } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index b1ddccb06..797837802 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -239,8 +241,7 @@ static class SkipNullsBuilt extends Values { } } - // A failing setter should be reported the same way with and without Blackbird. - // The throwable is created inside the setter, as a real one would be. + // A failing setter should be reported the same way with and without Blackbird static Supplier toThrow; static int setterCalls; @@ -271,7 +272,8 @@ public ThrowingBuilt build() { @JsonDeserialize(builder=ThrowingBuilder.class) static class ThrowingBuilt { } - // Fails inside the setter on the first call only, so a retry would succeed + // Fails on the first call only, so a retry would succeed + static Supplier flakyFailure; static int flakyCalls; static final class FlakyBuilder @@ -280,7 +282,11 @@ static final class FlakyBuilder public FlakyBuilder withStr(String v) { if (flakyCalls++ == 0) { - throw new SecurityException("thrown by setter"); + Throwable t = flakyFailure.get(); + if (t instanceof Error) { + throw (Error) t; + } + throw (RuntimeException) t; } str = v; return this; @@ -380,8 +386,9 @@ public void testNullReturningBuilder() throws Exception assertEquals(42, v.i); } - // An access failure of the generated accessor happens before the setter - // runs; the reflective fallback must still return the new builder + // An access failure is raised by the JVM in the generated accessor's own frame, + // before the setter runs; the anonymous classes stand in for that accessor. + // The reflective fallback must still return the new builder. @Test public void testAccessFailureFallsBackToReflection() throws Exception { @@ -391,21 +398,43 @@ public void testAccessFailureFallsBackToReflection() throws Exception BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( vanilla.constructType(CopyingBuilt.class)); SettableStringProperty prop = new SettableStringProperty(deser.findProperty("str"), - (bean, v) -> { throw new IllegalAccessError("simulated"); }, - (bean, v) -> { throw new IllegalAccessError("simulated"); }); + new BiConsumer() { + @Override + public void accept(Object bean, String v) { + throw new IllegalAccessError("simulated"); + } + }, + new BiFunction() { + @Override + public Object apply(Object bean, Object v) { + throw new IllegalAccessError("simulated"); + } + }); CopyingBuilder result = (CopyingBuilder) prop.setAndReturn(new CopyingBuilder(), "foo"); assertEquals("foo", result.str); assertEquals(1, result.reflectiveCalls); } + // Whether the setter creates the throwable or saved it earlier, it may already + // have done its work, so it must not run again @Test - public void testSetterSecurityExceptionIsNotRetried() throws Exception + public void testSetterFailureIsNotRetried() throws Exception { - flakyCalls = 0; - assertThrows(JsonMappingException.class, - () -> MAPPER.readValue(a2q("{'str':'foo'}"), FlakyBuilt.class)); - assertEquals(1, flakyCalls); + SecurityException savedSecurity = new SecurityException("saved"); + IllegalAccessError savedAccess = new IllegalAccessError("saved"); + List> failures = Arrays.asList( + () -> new SecurityException("thrown by setter"), + () -> new IllegalAccessError("thrown by setter"), + () -> savedSecurity, + () -> savedAccess); + for (Supplier failure : failures) { + flakyFailure = failure; + flakyCalls = 0; + assertThrows(JsonMappingException.class, + () -> MAPPER.readValue(a2q("{'str':'foo'}"), FlakyBuilt.class)); + assertEquals(1, flakyCalls, String.valueOf(failure.get())); + } } @Test @@ -425,13 +454,18 @@ public void testSkipNullsBuilder() throws Exception @Test public void testThrowingSetterMatchesVanilla() throws Exception { + SecurityException savedSecurity = new SecurityException("saved"); + IllegalAccessError savedAccess = new IllegalAccessError("saved"); List> failures = Arrays.asList( () -> new IllegalStateException("unchecked"), () -> new IllegalStateException("outer", new IllegalArgumentException("root")), () -> new IOException("bad value"), () -> new Exception("checked"), () -> new AssertionError("error"), - () -> new SecurityException("thrown by setter")); + () -> new SecurityException("thrown by setter"), + () -> new IllegalAccessError("thrown by setter"), + () -> savedSecurity, + () -> savedAccess); for (boolean wrap : new boolean[] { true, false }) { ObjectMapper vanilla = JsonMapper.builder() .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); From d7ef5bcf263f28775b7c4e2cbcf4d341a8e6d00c Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 28 Oct 2026 14:51:38 -0700 Subject: [PATCH 06/34] Add release notes --- release-notes/CREDITS-2.x | 5 +++++ release-notes/VERSION-2.x | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index aa75b5d3d..a38d3b7b4 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -166,3 +166,8 @@ Apoorva Manjunath (@apoorvam) * Reported, contributed fix for #285: `JacksonBlackbirdAccess` Class access exception with Groovy Script serialization using `BlackbirdModule` (2.19.0) + +Parker Winchester (@parker89) + +* Contributed #364: (blackbird) Use generated setters for Builder-based deserialization + (2.23.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index aa2889592..ad50e0e27 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -25,7 +25,8 @@ Active maintainers: 2.23.0 (not yet released) -No changes since 2.22 +#364: (blackbird) Use generated setters for Builder-based deserialization + (contributed by Parker W) 2.22.4 (17-Oct-2026) 2.22.3 (21-Sep-2026) From eca13bba5b4b88ccf21f8d368edbda436cd86bee Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 28 Oct 2026 20:39:05 -0400 Subject: [PATCH 07/34] Avoid retrying exceptions thrown by bean setters --- .../deser/OptimizedSettableBeanProperty.java | 25 ++- .../deser/SettableBooleanProperty.java | 4 +- .../blackbird/deser/SettableIntProperty.java | 4 +- .../blackbird/deser/SettableLongProperty.java | 4 +- .../deser/SettableObjectProperty.java | 2 +- .../deser/SettableStringProperty.java | 2 +- .../blackbird/deser/TestSetterFailures.java | 185 ++++++++++++++++++ .../deser/access/InaccessibleSetterValue.java | 23 +++ 8 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/access/InaccessibleSetterValue.java diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index be5d79b77..f8f06ed72 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -104,11 +104,10 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, * * @since 2.9 */ - protected void _reportProblem(Object bean, Object value, Throwable e) + protected void _reportProblem(Object accessor, Object thunk, Object bean, Object value, Throwable e) throws IOException { - if ((e instanceof IllegalAccessError) - || (e instanceof SecurityException)) { + if (_isAccessorAccessFailure(accessor, thunk, e)) { _logAccessProblem(bean, e); delegate.set(bean, value); return; @@ -134,7 +133,7 @@ protected void _reportProblem(Object bean, Object value, Throwable e) protected Object _reportProblemAndReturn(Object accessor, Object bean, Object value, Throwable e) throws IOException { - if (_isAccessorAccessFailure(accessor, e)) { + if (_isAccessorAccessFailure(accessor, null, e)) { _logAccessProblem(bean, e); return delegate.setAndReturn(bean, value); } @@ -149,7 +148,7 @@ protected Object _reportProblemAndReturn(Object accessor, Object bean, Object va // on top of the stack (or, where generated frames are hidden from stack traces, // this property's frame that called it). A throwable created anywhere else, // such as one the setter saved and throws later, keeps its original failure. - private boolean _isAccessorAccessFailure(Object accessor, Throwable e) { + private boolean _isAccessorAccessFailure(Object accessor, Object thunk, Throwable e) { if (!(e instanceof IllegalAccessError) && !(e instanceof SecurityException)) { return false; } @@ -158,7 +157,21 @@ private boolean _isAccessorAccessFailure(Object accessor, Throwable e) { return false; } String top = trace[0].getClassName(); - return top.equals(accessor.getClass().getName()) || top.equals(getClass().getName()); + if (top.equals(accessor.getClass().getName()) || top.equals(getClass().getName())) { + return true; + } + if (thunk == null) { + return false; + } + if (top.equals(thunk.getClass().getName())) { + return true; + } + // Regular fluent setters discard the result through a trampoline. When + // lambda frames are hidden, an access failure in the thunk points there. + String method = trace[0].getMethodName(); + return top.equals(BBDeserializerModifier.class.getName()) + && (method.equals("trampoline") || method.equals("intTrampoline") + || method.equals("longTrampoline") || method.equals("booleanTrampoline")); } private void _logAccessProblem(Object bean, Throwable e) { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 196ae6ca1..4dd42e794 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -48,7 +48,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(bean, b, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, b, e); } } @@ -73,7 +73,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(bean, b, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, b, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index ba5678688..669c321cc 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } @@ -68,7 +68,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index a800b4e45..49e2db56d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } @@ -67,7 +67,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index ccb5dd7de..a47e24ef0 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -89,7 +89,7 @@ public void set(Object bean, Object v) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 9958f26f8..e922f6534 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -59,7 +59,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, (String) value); } catch (Throwable e) { - _reportProblem(bean, value, e); + _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, value, e); } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java new file mode 100644 index 000000000..b9a2cbf3e --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -0,0 +1,185 @@ +package com.fasterxml.jackson.module.blackbird.deser; + +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; +import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; +import com.fasterxml.jackson.module.blackbird.deser.access.InaccessibleSetterValue; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestSetterFailures extends BlackbirdTestBase +{ + private static Supplier failure; + private static Throwable lastFailure; + private static int calls; + + static class VoidBean { + public void setStr(String value) { failOnce(); } + public void setObj(List value) { failOnce(); } + public void setI(int value) { failOnce(); } + public void setL(long value) { failOnce(); } + public void setB(boolean value) { failOnce(); } + } + + static class FluentBean { + public FluentBean setStr(String value) { failOnce(); return this; } + public FluentBean setObj(List value) { failOnce(); return this; } + public FluentBean setI(int value) { failOnce(); return this; } + public FluentBean setL(long value) { failOnce(); return this; } + public FluentBean setB(boolean value) { failOnce(); return this; } + } + + // A retry would succeed and hide the original failure. + private static void failOnce() { + if (calls++ == 0) { + lastFailure = failure.get(); + if (lastFailure instanceof Error) { + throw (Error) lastFailure; + } + throw (RuntimeException) lastFailure; + } + } + + private List> failures() { + SecurityException savedSecurity = new SecurityException("saved"); + IllegalAccessError savedAccess = new IllegalAccessError("saved"); + SecurityException emptySecurity = new SecurityException("no trace"); + emptySecurity.setStackTrace(new StackTraceElement[0]); + IllegalAccessError emptyAccess = new IllegalAccessError("no trace"); + emptyAccess.setStackTrace(new StackTraceElement[0]); + return Arrays.asList( + () -> new SecurityException("thrown by setter"), + () -> new IllegalAccessError("thrown by setter"), + () -> savedSecurity, + () -> savedAccess, + () -> emptySecurity, + () -> emptyAccess); + } + + @Test + public void testDeserializeDoesNotRetrySetterFailures() throws Exception { + for (boolean wrap : new boolean[] { true, false }) { + ObjectMapper mapper = mapperBuilder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + for (Class beanType : new Class[] { VoidBean.class, FluentBean.class }) { + for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'obj':['foo']}"), + a2q("{'i':42}"), a2q("{'l':12345678901}"), a2q("{'b':true}") }) { + for (Supplier nextFailure : failures()) { + failure = nextFailure; + assertRunsOnce(() -> mapper.readValue(json, beanType), + beanType.getSimpleName() + ", wrap=" + wrap + ", " + json); + } + } + } + } + } + + @Test + public void testSetDoesNotRetrySetterFailures() throws Exception { + ObjectMapper mapper = newObjectMapper(); + String[] names = { "str", "obj", "i", "l", "b" }; + Object[] values = { "foo", Arrays.asList("foo"), 42, 12345678901L, true }; + List> beans = Arrays.asList(VoidBean::new, FluentBean::new); + for (Supplier newBean : beans) { + Class beanType = newBean.get().getClass(); + for (int i = 0; i < names.length; i++) { + SettableBeanProperty property = property(mapper, beanType, names[i]); + assertTrue(property instanceof OptimizedSettableBeanProperty, names[i]); + Object value = values[i]; + for (Supplier nextFailure : failures()) { + failure = nextFailure; + assertRunsOnce(() -> property.set(newBean.get(), value), + beanType.getSimpleName() + ", " + names[i]); + } + } + } + } + + private void assertRunsOnce(Executable action, String description) { + calls = 0; + Throwable thrown = assertThrows(Throwable.class, action, description); + assertEquals(1, calls, description); + while (thrown.getCause() != null) { + thrown = thrown.getCause(); + } + assertSame(lastFailure, thrown, description); + } + + @Test + public void testVoidAccessorAccessFailureFallsBack() throws Throwable { + InaccessibleSetterValue bean = new InaccessibleSetterValue(); + Object value = InaccessibleSetterValue.newValue(); + Method setter = InaccessibleSetterValue.class.getMethod("setVoidValue", value.getClass()); + BiConsumer accessor = voidAccessor(setter); + SettableObjectProperty property = new SettableObjectProperty( + property(newVanillaJSONMapper(), bean.getClass(), "voidValue"), accessor, null); + + // The generated accessor cannot cast to the package-private parameter type. + assertThrows(IllegalAccessError.class, () -> accessor.accept(bean, value)); + assertEquals(0, bean.calls); + property.set(bean, value); + assertEquals(1, bean.calls); + assertSame(value, bean.value); + } + + @Test + public void testFluentAccessorAccessFailureFallsBack() throws Throwable { + InaccessibleSetterValue bean = new InaccessibleSetterValue(); + Object value = InaccessibleSetterValue.newValue(); + Method setter = InaccessibleSetterValue.class.getMethod("setFluentValue", value.getClass()); + BiFunction thunk = returningAccessor(setter); + BiConsumer accessor = (instance, v) -> BBDeserializerModifier.trampoline(thunk, instance, v); + SettableObjectProperty property = new SettableObjectProperty( + property(newVanillaJSONMapper(), bean.getClass(), "fluentValue"), accessor, thunk); + + // With hidden lambda frames, the error's top frame is the trampoline. + assertThrows(IllegalAccessError.class, () -> accessor.accept(bean, value)); + assertEquals(0, bean.calls); + property.set(bean, value); + assertEquals(1, bean.calls); + assertSame(value, bean.value); + } + + private SettableBeanProperty property(ObjectMapper mapper, Class beanType, String name) throws Exception { + DefaultDeserializationContext context = ((DefaultDeserializationContext) mapper.getDeserializationContext()) + .createInstance(mapper.getDeserializationConfig(), null, null); + BeanDeserializerBase deserializer = (BeanDeserializerBase) context.findRootValueDeserializer( + mapper.constructType(beanType)); + return deserializer.findProperty(name); + } + + @SuppressWarnings("unchecked") + private BiConsumer voidAccessor(Method setter) throws Throwable { + MethodHandle handle = MethodHandles.lookup().unreflect(setter); + return (BiConsumer) LambdaMetafactory.metafactory( + MethodHandles.lookup(), "accept", MethodType.methodType(BiConsumer.class), + MethodType.methodType(void.class, Object.class, Object.class), handle, handle.type()) + .getTarget().invoke(); + } + + @SuppressWarnings("unchecked") + private BiFunction returningAccessor(Method setter) throws Throwable { + MethodHandle handle = MethodHandles.lookup().unreflect(setter); + return (BiFunction) LambdaMetafactory.metafactory( + MethodHandles.lookup(), "apply", MethodType.methodType(BiFunction.class), + MethodType.methodType(Object.class, Object.class, Object.class), handle, handle.type()) + .getTarget().invoke(); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/access/InaccessibleSetterValue.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/access/InaccessibleSetterValue.java new file mode 100644 index 000000000..e56035964 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/access/InaccessibleSetterValue.java @@ -0,0 +1,23 @@ +package com.fasterxml.jackson.module.blackbird.deser.access; + +public class InaccessibleSetterValue { + public int calls; + public Object value; + + public void setVoidValue(SetterValue value) { + calls++; + this.value = value; + } + + public InaccessibleSetterValue setFluentValue(SetterValue value) { + calls++; + this.value = value; + return this; + } + + public static Object newValue() { + return new SetterValue(); + } +} + +class SetterValue { } From 330ac204c77dd65477d30e554bc944d62df53b28 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 28 Oct 2026 20:53:11 -0400 Subject: [PATCH 08/34] Report JSON location for builder setter failures --- .../deser/OptimizedSettableBeanProperty.java | 9 ++++--- .../deser/SettableBooleanProperty.java | 20 ++++++++------- .../blackbird/deser/SettableIntProperty.java | 10 ++++---- .../blackbird/deser/SettableLongProperty.java | 10 ++++---- .../deser/SettableObjectProperty.java | 11 ++++++-- .../deser/SettableStringProperty.java | 10 +++++--- .../module/blackbird/deser/TestBuilders.java | 25 ++++++++++++++++++- 7 files changed, 67 insertions(+), 28 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index be5d79b77..a6bb96dc5 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -129,9 +129,12 @@ protected void _reportProblem(Object bean, Object value, Throwable e) /** * Like {@link #_reportProblem}, for {@link #setAndReturn}: an access failure of * the generated accessor falls back to the delegate, and anything the setter - * throws is reported the way {@code MethodProperty.setAndReturn} reports it. + * throws is reported the way {@code MethodProperty} reports it. The parser is + * only used for the error location, and is null outside of + * {@link #deserializeSetAndReturn}, as it is for {@code MethodProperty}. */ - protected Object _reportProblemAndReturn(Object accessor, Object bean, Object value, Throwable e) + protected Object _reportProblemAndReturn(JsonParser p, Object accessor, Object bean, Object value, + Throwable e) throws IOException { if (_isAccessorAccessFailure(accessor, e)) { @@ -140,7 +143,7 @@ protected Object _reportProblemAndReturn(Object accessor, Object bean, Object va } // `Method.invoke` wraps whatever the setter throws, and `MethodProperty` // translates that wrapper; doing the same keeps type, message, cause and path - _throwAsIOE(new InvocationTargetException(e), value); + _throwAsIOE(p, new InvocationTargetException(e), value); return null; // never gets here } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 196ae6ca1..9ec814216 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -56,12 +56,14 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - JsonToken t = p.currentToken(); - if (t == JsonToken.VALUE_TRUE) { - return _setAndReturn(instance, true); - } - if (t == JsonToken.VALUE_FALSE) { - return _setAndReturn(instance, false); + if (_optimizedSetterAndReturn != null) { + JsonToken t = p.currentToken(); + if (t == JsonToken.VALUE_TRUE) { + return _setAndReturn(p, instance, true); + } + if (t == JsonToken.VALUE_FALSE) { + return _setAndReturn(p, instance, false); + } } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -83,10 +85,10 @@ public Object setAndReturn(Object instance, Object value) throws IOException { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: - return _setAndReturn(instance, ((Boolean) value).booleanValue()); + return _setAndReturn(null, instance, ((Boolean) value).booleanValue()); } - private Object _setAndReturn(Object instance, boolean b) throws IOException { + private Object _setAndReturn(JsonParser p, Object instance, boolean b) throws IOException { if (_optimizedSetterAndReturn == null) { return delegate.setAndReturn(instance, b); } @@ -94,7 +96,7 @@ private Object _setAndReturn(Object instance, boolean b) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, b); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, b, e); + return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, b, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index ba5678688..09a6a0641 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -55,8 +55,8 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if (p.isExpectedNumberIntToken()) { - return _setAndReturn(instance, p.getIntValue()); + if ((_optimizedSetterAndReturn != null) && p.isExpectedNumberIntToken()) { + return _setAndReturn(p, instance, p.getIntValue()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -78,10 +78,10 @@ public Object setAndReturn(Object instance, Object value) throws IOException { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: - return _setAndReturn(instance, ((Number) value).intValue()); + return _setAndReturn(null, instance, ((Number) value).intValue()); } - private Object _setAndReturn(Object instance, int v) throws IOException { + private Object _setAndReturn(JsonParser p, Object instance, int v) throws IOException { if (_optimizedSetterAndReturn == null) { return delegate.setAndReturn(instance, v); } @@ -89,7 +89,7 @@ private Object _setAndReturn(Object instance, int v) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, v, e); + return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index a800b4e45..cdb59619e 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -54,8 +54,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if (p.isExpectedNumberIntToken()) { - return _setAndReturn(instance, p.getLongValue()); + if ((_optimizedSetterAndReturn != null) && p.isExpectedNumberIntToken()) { + return _setAndReturn(p, instance, p.getLongValue()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -77,10 +77,10 @@ public Object setAndReturn(Object instance, Object value) throws IOException { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: - return _setAndReturn(instance, ((Number) value).longValue()); + return _setAndReturn(null, instance, ((Number) value).longValue()); } - private Object _setAndReturn(Object instance, long v) throws IOException { + private Object _setAndReturn(JsonParser p, Object instance, long v) throws IOException { if (_optimizedSetterAndReturn == null) { return delegate.setAndReturn(instance, v); } @@ -88,7 +88,7 @@ private Object _setAndReturn(Object instance, long v) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, v, e); + return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index ccb5dd7de..48ff27c4f 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -63,6 +63,9 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { + if (_optimizedSetterAndReturn == null) { + return delegate.deserializeSetAndReturn(p, ctxt, instance); + } Object value; if (p.hasToken(JsonToken.VALUE_NULL)) { if (_skipNulls) { @@ -81,7 +84,7 @@ public Object deserializeSetAndReturn(JsonParser p, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } - return setAndReturn(instance, value); + return _setAndReturn(p, instance, value); } @Override @@ -98,6 +101,10 @@ public Object setAndReturn(Object instance, Object value) throws IOException { if (_optimizedSetterAndReturn == null) { return delegate.setAndReturn(instance, value); } + return _setAndReturn(null, instance, value); + } + + private Object _setAndReturn(JsonParser p, Object instance, Object value) throws IOException { if ((value == null) && _skipNulls) { return instance; } @@ -105,7 +112,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, value, e); + return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, value, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 9958f26f8..16735ed4d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -48,8 +48,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object @Override public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if (p.hasToken(JsonToken.VALUE_STRING)) { - return setAndReturn(instance, p.getText()); + if ((_optimizedSetterAndReturn != null) && p.hasToken(JsonToken.VALUE_STRING)) { + return _setAndReturn(p, instance, p.getText()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); } @@ -68,6 +68,10 @@ public Object setAndReturn(Object instance, Object value) throws IOException { if (_optimizedSetterAndReturn == null) { return delegate.setAndReturn(instance, value); } + return _setAndReturn(null, instance, value); + } + + private Object _setAndReturn(JsonParser p, Object instance, Object value) throws IOException { if ((value == null) && _skipNulls) { return instance; } @@ -75,7 +79,7 @@ public Object setAndReturn(Object instance, Object value) throws IOException { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(_optimizedSetterAndReturn, instance, value, e); + return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, value, e); } } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 797837802..cf910f44e 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -251,10 +251,26 @@ public ThrowingBuilder withStr(String v) throws Exception { return fail(); } + public ThrowingBuilder withList(List v) throws Exception { + return fail(); + } + public ThrowingBuilder withI(int v) throws Exception { return fail(); } + public ThrowingBuilder withL(long v) throws Exception { + return fail(); + } + + public ThrowingBuilder withB(boolean v) throws Exception { + return fail(); + } + + public void withV(String v) throws Exception { + fail(); + } + private static ThrowingBuilder fail() throws Exception { setterCalls++; Throwable t = toThrow.get(); @@ -466,13 +482,16 @@ public void testThrowingSetterMatchesVanilla() throws Exception () -> new IllegalAccessError("thrown by setter"), () -> savedSecurity, () -> savedAccess); + // void setters still go through the delegate + String voidJson = a2q("{'v':'foo'}"); for (boolean wrap : new boolean[] { true, false }) { ObjectMapper vanilla = JsonMapper.builder() .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); ObjectMapper blackbird = mapperBuilder() .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); for (Supplier failure : failures) { - for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'i':42}") }) { + for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'list':['a']}"), + a2q("{'i':42}"), a2q("{'l':42}"), a2q("{'b':true}"), voidJson }) { toThrow = failure; setterCalls = 0; Throwable expected = assertThrows(Throwable.class, @@ -482,6 +501,9 @@ public void testThrowingSetterMatchesVanilla() throws Exception String desc = "wrap=" + wrap + ", " + json + ", " + expected; assertEquals(2, setterCalls, desc); _assertSameFailure(desc, expected, actual); + if (json.equals(voidJson)) { + continue; + } // and the Blackbird failure came from the generated accessor Throwable root = actual; while (root.getCause() != null) { @@ -501,6 +523,7 @@ private static void _assertSameFailure(String desc, Throwable expected, Throwabl JsonMappingException a = (JsonMappingException) actual; assertEquals(e.getOriginalMessage(), a.getOriginalMessage(), desc); assertEquals(e.getPathReference(), a.getPathReference(), desc); + assertEquals(e.getLocation(), a.getLocation(), desc); } else { assertEquals(expected.getMessage(), actual.getMessage(), desc); } From dd47bebbe8685fca77c26112a2241ee7bb5f50bb Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 28 Oct 2026 20:56:42 -0400 Subject: [PATCH 09/34] Skip generated setters for types the accessor can't access --- .../deser/BBDeserializerModifier.java | 13 + .../deser/OptimizedSettableBeanProperty.java | 13 +- .../module/blackbird/util/ReflectionHack.java | 103 ++++++++ .../module/blackbird/deser/TestBuilders.java | 104 +++++++- .../deser/TestInaccessibleSetterTypes.java | 240 ++++++++++++++++++ .../blackbird/deser/otherpackage/Mode.java | 4 + .../deser/otherpackage/ModeBeanBase.java | 21 ++ .../deser/otherpackage/ModeBuilderBase.java | 30 +++ .../blackbird/deser/shadowed/Chain.java | 3 + .../deser/shadowed/ChainBuilder.java | 8 + .../deser/shadowed/ChainBuilderBase.java | 14 + .../blackbird/deser/shadowed/ChainBuilt.java | 13 + .../blackbird/deser/shadowed/Level.java | 3 + .../blackbird/deser/shadowed/LevelBean.java | 3 + .../deser/shadowed/LevelBeanBase.java | 15 ++ .../deser/shadowed/LevelBuilder.java | 8 + .../deser/shadowed/LevelBuilderBase.java | 15 ++ .../blackbird/deser/shadowed/LevelBuilt.java | 13 + .../blackbird/util/TestReflectionHack.java | 45 ++++ 19 files changed, 663 insertions(+), 5 deletions(-) create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBuilderBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilder.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilderBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilt.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilder.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilderBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilt.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 07f803de2..231b485cb 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -178,6 +178,19 @@ private void nextProperty(SettableBeanProperty prop, return; //setter = lookup.unreflectGetter((Field) jdkMember); } + // The generated accessor casts to each type in the setter's signature, so + // one it can't access (like a package-private type from another package) + // would fail on every call; leave those to reflection + for (Class paramType : setter.type().parameterArray()) { + if (!ReflectionHack.canAccessClass(lookup, paramType)) { + return; + } + } + // Calling the setter also names its return type, which nothing casts to, but + // the JVM still requires to be the same class from the accessor's loader + if (!ReflectionHack.canResolveClass(lookup, setter.type().returnType())) { + return; + } if (type.isPrimitive()) { if (type == Integer.TYPE) { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index a6bb96dc5..85a0a99c9 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -137,7 +137,7 @@ protected Object _reportProblemAndReturn(JsonParser p, Object accessor, Object b Throwable e) throws IOException { - if (_isAccessorAccessFailure(accessor, e)) { + if (_isAccessorAccessFailure(accessor, bean, e)) { _logAccessProblem(bean, e); return delegate.setAndReturn(bean, value); } @@ -152,16 +152,23 @@ protected Object _reportProblemAndReturn(JsonParser p, Object accessor, Object b // on top of the stack (or, where generated frames are hidden from stack traces, // this property's frame that called it). A throwable created anywhere else, // such as one the setter saved and throws later, keeps its original failure. - private boolean _isAccessorAccessFailure(Object accessor, Throwable e) { + private boolean _isAccessorAccessFailure(Object accessor, Object bean, Throwable e) { if (!(e instanceof IllegalAccessError) && !(e instanceof SecurityException)) { return false; } StackTraceElement[] trace = e.getStackTrace(); + // No trace (stack traces disabled, or a stackless throwable) is no evidence; + // setup already skips the accessors known to fail this way if (trace.length == 0) { return false; } String top = trace[0].getClassName(); - return top.equals(accessor.getClass().getName()) || top.equals(getClass().getName()); + if (top.equals(accessor.getClass().getName())) { + return true; + } + // A setter in a hidden class is left out of the trace too, which is only + // possible if the bean's own class is hidden (their names contain a '/') + return top.equals(getClass().getName()) && (bean.getClass().getName().indexOf('/') < 0); } private void _logAccessProblem(Object bean, Throwable e) { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java index eb0f9e978..b6fb29397 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java @@ -4,6 +4,7 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.util.logging.Level; import java.util.logging.Logger; @@ -27,11 +28,91 @@ public static Lookup privateLookupIn(Class lookup, MethodHandles.Lookup orig) return Java8.privateLookupIn(lookup); } + /** + * Whether code defined with {@code lookup}, such as a lambda it spins, can use + * {@code type} where it must also access it, as in a cast. If not, that code + * fails when it runs, not when it is created. + */ + public static boolean canAccessClass(Lookup lookup, Class type) { + type = elementType(type); + if (type.isPrimitive()) { + return true; + } + try { + if (!resolvesTo(lookup, type)) { + return false; + } + if (Java9Up.ACCESS_CLASS != null) { + return Java9Up.canAccessClass(lookup, type); + } + return canAccessClassWithoutModules(lookup.lookupClass(), type); + } catch (SecurityException e) { + // e.g. a security manager denying `getClassLoader` + return false; + } + } + + /** + * Like {@link #canAccessClass}, for a type that code only names, as in the + * descriptor of a method it calls: it must resolve to the same class, but + * need not be accessible. + */ + public static boolean canResolveClass(Lookup lookup, Class type) { + type = elementType(type); + if (type.isPrimitive()) { + return true; + } + try { + return resolvesTo(lookup, type); + } catch (SecurityException e) { + return false; + } + } + + private static Class elementType(Class type) { + while (type.isArray()) { + type = type.getComponentType(); + } + return type; + } + + // That code refers to `type` by name, resolved by the lookup class's loader, + // which may find a different class of the same name (e.g. a child-first loader + // with its own copy) or none at all + private static boolean resolvesTo(Lookup lookup, Class type) { + try { + return Class.forName(type.getName(), false, lookup.lookupClass().getClassLoader()) == type; + } catch (ClassNotFoundException | LinkageError e) { + return false; + } + } + + // Without modules, public classes are accessible from anywhere, others only + // from the same runtime package (which includes the bean's own class, often + // package-private). `getModifiers` reports the declared modifiers of nested + // classes, so a protected one is (safely) treated as inaccessible from other + // packages, though the JVM would allow it. + static boolean canAccessClassWithoutModules(Class lookupClass, Class type) { + if (Modifier.isPublic(type.getModifiers())) { + return true; + } + return (type.getClassLoader() == lookupClass.getClassLoader()) + && packageName(type).equals(packageName(lookupClass)); + } + + private static String packageName(Class type) { + String name = type.getName(); + int dot = name.lastIndexOf('.'); + return (dot < 0) ? "" : name.substring(0, dot); + } + static class Java9Up { static final MethodHandle FACTORY; + static final MethodHandle ACCESS_CLASS; static { FACTORY = init(); + ACCESS_CLASS = (FACTORY == null) ? null : initAccessClass(); } private static MethodHandle init() { @@ -51,6 +132,17 @@ private static MethodHandle init() { } } + private static MethodHandle initAccessClass() { + try { + return MethodHandles.lookup().findVirtual( + MethodHandles.Lookup.class, + "accessClass", + MethodType.methodType(Class.class, Class.class)); + } catch (ReflectiveOperationException e) { + return null; + } + } + public static Lookup privateLookupIn(Class lookup, Lookup orig) { try { return (Lookup) FACTORY.invokeExact(lookup, orig); @@ -60,6 +152,17 @@ public static Lookup privateLookupIn(Class lookup, Lookup orig) { throw Sneaky.throwAnyway(t); } } + + public static boolean canAccessClass(Lookup lookup, Class type) { + try { + Class unused = (Class) ACCESS_CLASS.invokeExact(lookup, type); + return true; + } catch (IllegalAccessException e) { + return false; + } catch (Throwable t) { + throw Sneaky.throwAnyway(t); + } + } } static class Java8 { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index cf910f44e..b8b189acb 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -1,6 +1,11 @@ package com.fasterxml.jackson.module.blackbird.deser; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Array; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; import java.util.function.BiConsumer; @@ -9,6 +14,7 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -17,11 +23,13 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; +import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.impl.MethodProperty; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public class TestBuilders extends BlackbirdTestBase { @@ -316,6 +324,41 @@ public FlakyBuilt build() { @JsonDeserialize(builder=FlakyBuilder.class) static class FlakyBuilt { } + // The builder is a hidden subclass whose setter fails after doing its work. Hidden + // frames are left out of stack traces, as the generated accessor's are. + static Class hiddenBuilderClass; + static int hiddenCalls; + + static class HideableBuilder + { + @JsonCreator + public static HideableBuilder create() throws Exception { + return (HideableBuilder) hiddenBuilderClass.getConstructor().newInstance(); + } + + public HideableBuilder withStr(String v) { + return this; + } + + public HideableBuilt build() { + return new HideableBuilt(); + } + } + + public static class HiddenBuilder extends HideableBuilder + { + public HiddenBuilder() { } + + @Override + public HideableBuilder withStr(String v) { + hiddenCalls++; + throw new SecurityException("thrown by hidden setter"); + } + } + + @JsonDeserialize(builder=HideableBuilder.class) + static class HideableBuilt { } + static class Values { final String str; final List list; @@ -433,7 +476,8 @@ public Object apply(Object bean, Object v) { } // Whether the setter creates the throwable or saved it earlier, it may already - // have done its work, so it must not run again + // have done its work, so it must not run again. That includes throwables with no + // stack trace, as with `-XX:-StackTraceInThrowable`. @Test public void testSetterFailureIsNotRetried() throws Exception { @@ -443,7 +487,9 @@ public void testSetterFailureIsNotRetried() throws Exception () -> new SecurityException("thrown by setter"), () -> new IllegalAccessError("thrown by setter"), () -> savedSecurity, - () -> savedAccess); + () -> savedAccess, + () -> _withoutStackTrace(new SecurityException("stackless")), + () -> _withoutStackTrace(new IllegalAccessError("stackless"))); for (Supplier failure : failures) { flakyFailure = failure; flakyCalls = 0; @@ -516,6 +562,60 @@ public void testThrowingSetterMatchesVanilla() throws Exception } } + // A setter in a hidden class looks like the generated accessor in a stack trace + // (both are left out), so its failure must not be mistaken for an accessor's + @Test + public void testHiddenClassSetterFailureIsNotRetried() throws Exception + { + Method define; + Object noOptions; + try { + Class option = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption"); + noOptions = Array.newInstance(option, 0); + define = MethodHandles.Lookup.class.getMethod("defineHiddenClass", + byte[].class, boolean.class, noOptions.getClass()); + } catch (ReflectiveOperationException e) { + assumeTrue(false, "hidden classes need Java 15+"); + return; + } + byte[] bytes; + try (InputStream in = getClass().getResourceAsStream( + "/" + HiddenBuilder.class.getName().replace('.', '/') + ".class")) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + } + bytes = out.toByteArray(); + } + hiddenBuilderClass = ((MethodHandles.Lookup) define.invoke(MethodHandles.lookup(), bytes, true, noOptions)) + .lookupClass(); + assertTrue(_findProperty(MAPPER, HideableBuilt.class, "str") instanceof OptimizedSettableBeanProperty); + + for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), MAPPER }) { + hiddenCalls = 0; + assertThrows(JsonMappingException.class, + () -> mapper.readValue(a2q("{'str':'foo'}"), HideableBuilt.class)); + assertEquals(1, hiddenCalls); + } + } + + private static SettableBeanProperty _findProperty(ObjectMapper mapper, Class type, String name) + throws Exception + { + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) mapper.getDeserializationContext()) + .createInstance(mapper.getDeserializationConfig(), null, null); + BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( + mapper.constructType(type)); + return deser.findProperty(name); + } + + private static T _withoutStackTrace(T t) { + t.setStackTrace(new StackTraceElement[0]); + return t; + } + private static void _assertSameFailure(String desc, Throwable expected, Throwable actual) { assertEquals(expected.getClass(), actual.getClass(), desc); if (expected instanceof JsonMappingException) { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java new file mode 100644 index 000000000..4fb6e22f6 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -0,0 +1,240 @@ +package com.fasterxml.jackson.module.blackbird.deser; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; +import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.databind.deser.impl.MethodProperty; +import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ModeBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ModeBuilderBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.Chain; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.ChainBuilder; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.ChainBuilderBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.ChainBuilt; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.Level; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBean; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBuilder; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBuilderBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBuilt; + +import static org.junit.jupiter.api.Assertions.*; + +// The inherited setters are public, but `withMode` and `setMode` take a type that is +// package-private in another package. Reflection can call them, but a generated +// accessor can't refer to that type, and would fail on every call. +public class TestInaccessibleSetterTypes extends BlackbirdTestBase +{ + static final class ModeBuilder extends ModeBuilderBase + { + public ModeBuilt build() { + return new ModeBuilt(mode, modes, name); + } + } + + @JsonDeserialize(builder=ModeBuilder.class) + static class ModeBuilt + { + final String mode; + final String modes; + final String name; + + ModeBuilt(String mode, String modes, String name) { + this.mode = mode; + this.modes = modes; + this.name = name; + } + } + + static final class ModeBean extends ModeBeanBase { } + + private static final String JSON = a2q("{'mode':'FAST','name':'foo'}"); + + private final ObjectMapper MAPPER = newObjectMapper(); + + @Test + public void testBuilder() throws Exception + { + ModeBuilderBase.setterCalls = 0; + ModeBuilt result = MAPPER.readValue(a2q("{'mode':'FAST','modes':['SAFE'],'name':'foo'}"), + ModeBuilt.class); + assertEquals("FAST", result.mode); + assertEquals("[SAFE]", result.modes); + assertEquals("foo", result.name); + assertEquals(2, ModeBuilderBase.setterCalls); + _assertOnlyModeUsesReflection(ModeBuilt.class); + // and so does an array of it + assertEquals(MethodProperty.class, _findProperty(ModeBuilt.class, "modes").getClass()); + } + + @Test + public void testBean() throws Exception + { + ModeBeanBase.setterCalls = 0; + ModeBean result = MAPPER.readValue(JSON, ModeBean.class); + assertEquals("FAST", result.mode()); + assertEquals("foo", result.name()); + assertEquals(1, ModeBeanBase.setterCalls); + _assertOnlyModeUsesReflection(ModeBean.class); + } + + // The loader of the builder and bean has its own copy of `Level`, so a generated + // accessor would resolve that one and fail to cast the inherited setter's `Level` + @Test + public void testShadowedType() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Level.class, LevelBuilder.class, LevelBuilt.class, LevelBean.class)); + Class built = Class.forName(LevelBuilt.class.getName(), true, loader); + Class bean = Class.forName(LevelBean.class.getName(), true, loader); + assertNotSame(LevelBuilt.class, built); + assertNotSame(LevelBean.class, bean); + + LevelBuilderBase.setterCalls = 0; + Object result = MAPPER.readValue(a2q("{'level':'HIGH'}"), built); + assertEquals("HIGH", built.getField("level").get(result)); + assertEquals(1, LevelBuilderBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(built, "level").getClass()); + + LevelBeanBase.setterCalls = 0; + LevelBeanBase beanResult = (LevelBeanBase) MAPPER.readValue(a2q("{'level':'HIGH'}"), bean); + assertEquals("HIGH", beanResult.level()); + assertEquals(1, LevelBeanBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(bean, "level").getClass()); + } + + // Same for the setter's return type: nothing casts to it, but calling the setter + // names it, and the JVM requires both loaders to agree on it + @Test + public void testShadowedReturnType() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Chain.class, ChainBuilder.class, ChainBuilt.class)); + Class built = Class.forName(ChainBuilt.class.getName(), true, loader); + assertNotSame(ChainBuilt.class, built); + // the builder's loader has its own `Chain`, as it would once anything there used it + assertNotSame(Chain.class, Class.forName(Chain.class.getName(), false, loader)); + + ChainBuilderBase.setterCalls = 0; + Object result = MAPPER.readValue(a2q("{'level':'HIGH'}"), built); + assertEquals("HIGH", built.getField("level").get(result)); + assertEquals(1, ChainBuilderBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(built, "level").getClass()); + } + + // Setup skips this accessor, so build it the same way here to check that the + // access error the JVM really raises (from a hidden frame on Java 9+) is + // recognized as the accessor's, and falls back without running the setter twice + @Test + public void testRealAccessorFailureFallsBack() throws Throwable + { + Class mode = Class.forName(ModeBuilderBase.class.getPackage().getName() + ".Mode"); + MethodHandle setter = MethodHandles.lookup().unreflect( + ModeBuilderBase.class.getMethod("withMode", mode)); + @SuppressWarnings("unchecked") + BiFunction thunk = (BiFunction) LambdaMetafactory.metafactory( + MethodHandles.lookup(), "apply", + MethodType.methodType(BiFunction.class), + MethodType.methodType(Object.class, Object.class, Object.class), + setter, setter.type()) + .getTarget().invoke(); + BiConsumer unused = (bean, value) -> fail("not a builder setter"); + + ObjectMapper vanilla = newVanillaJSONMapper(); + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) vanilla.getDeserializationContext()) + .createInstance(vanilla.getDeserializationConfig(), null, null); + BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( + vanilla.constructType(ModeBuilt.class)); + SettableObjectProperty prop = new SettableObjectProperty(deser.findProperty("mode"), unused, thunk); + + Object fast = mode.getEnumConstants()[0]; + for (int i = 0; i < 2; i++) { + ModeBuilderBase.setterCalls = 0; + ModeBuilder builder = new ModeBuilder(); + assertThrows(IllegalAccessError.class, () -> thunk.apply(builder, fast)); + assertEquals(0, ModeBuilderBase.setterCalls); + assertSame(builder, prop.setAndReturn(builder, fast)); + assertEquals("FAST", builder.build().mode); + assertEquals(1, ModeBuilderBase.setterCalls); + } + } + + private void _assertOnlyModeUsesReflection(Class type) throws Exception + { + assertEquals(MethodProperty.class, _findProperty(type, "mode").getClass()); + SettableBeanProperty name = _findProperty(type, "name"); + assertTrue(name instanceof OptimizedSettableBeanProperty, name.getClass().getName()); + } + + private SettableBeanProperty _findProperty(Class type, String name) throws Exception + { + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) MAPPER.getDeserializationContext()) + .createInstance(MAPPER.getDeserializationConfig(), null, null); + BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( + MAPPER.constructType(type)); + return deser.findProperty(name); + } + + // Defines its own copies of the given classes, delegating everything else + static final class ChildFirstLoader extends ClassLoader + { + private final List> _copied; + + ChildFirstLoader(ClassLoader parent, List> copied) { + super(parent); + _copied = copied; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + for (Class copied : _copied) { + if (copied.getName().equals(name)) { + byte[] bytes = _bytesOf(copied); + c = defineClass(name, bytes, 0, bytes.length); + } + } + } + if (c == null) { + return super.loadClass(name, resolve); + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + private byte[] _bytesOf(Class type) throws ClassNotFoundException { + String resource = type.getName().replace('.', '/') + ".class"; + try (InputStream in = getParent().getResourceAsStream(resource)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + } + return out.toByteArray(); + } catch (Exception e) { + throw new ClassNotFoundException(type.getName(), e); + } + } + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java new file mode 100644 index 000000000..1a8c0d17d --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java @@ -0,0 +1,4 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +// package-private, so subclasses in other packages can't refer to it +enum Mode { FAST, SAFE } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java new file mode 100644 index 000000000..95f1a6826 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +public class ModeBeanBase +{ + public static int setterCalls; + + protected String mode; + protected String name; + + public void setMode(Mode m) { + setterCalls++; + mode = m.name(); + } + + public void setName(String n) { + name = n; + } + + public String mode() { return mode; } + public String name() { return name; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBuilderBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBuilderBase.java new file mode 100644 index 000000000..92aacb639 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBuilderBase.java @@ -0,0 +1,30 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +public class ModeBuilderBase> +{ + public static int setterCalls; + + protected String mode; + protected String modes; + protected String name; + + @SuppressWarnings("unchecked") + public T withMode(Mode m) { + setterCalls++; + mode = m.name(); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withModes(Mode[] m) { + setterCalls++; + modes = java.util.Arrays.toString(m); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withName(String n) { + name = n; + return (T) this; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java new file mode 100644 index 000000000..1b8180dea --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public interface Chain { } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilder.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilder.java new file mode 100644 index 000000000..41ea29888 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilder.java @@ -0,0 +1,8 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public final class ChainBuilder extends ChainBuilderBase +{ + public ChainBuilt build() { + return new ChainBuilt(level); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilderBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilderBase.java new file mode 100644 index 000000000..8b1814b3f --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilderBase.java @@ -0,0 +1,14 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class ChainBuilderBase implements Chain +{ + public static int setterCalls; + + protected String level; + + public Chain withLevel(String l) { + setterCalls++; + level = l; + return this; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilt.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilt.java new file mode 100644 index 000000000..c76dd7130 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBuilt.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder=ChainBuilder.class) +public class ChainBuilt +{ + public final String level; + + ChainBuilt(String level) { + this.level = level; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java new file mode 100644 index 000000000..cf775a818 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public enum Level { LOW, HIGH } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java new file mode 100644 index 000000000..a87ae2359 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class LevelBean extends LevelBeanBase { } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java new file mode 100644 index 000000000..8e043b0f9 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java @@ -0,0 +1,15 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class LevelBeanBase +{ + public static int setterCalls; + + protected String level; + + public void setLevel(Level l) { + setterCalls++; + level = l.name(); + } + + public String level() { return level; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilder.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilder.java new file mode 100644 index 000000000..cfcd29c65 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilder.java @@ -0,0 +1,8 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public final class LevelBuilder extends LevelBuilderBase +{ + public LevelBuilt build() { + return new LevelBuilt(level); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilderBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilderBase.java new file mode 100644 index 000000000..90569e5a2 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilderBase.java @@ -0,0 +1,15 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class LevelBuilderBase> +{ + public static int setterCalls; + + protected String level; + + @SuppressWarnings("unchecked") + public T withLevel(Level l) { + setterCalls++; + level = l.name(); + return (T) this; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilt.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilt.java new file mode 100644 index 000000000..2b1ddcc48 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBuilt.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder=LevelBuilder.class) +public class LevelBuilt +{ + public final String level; + + LevelBuilt(String level) { + this.level = level; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java new file mode 100644 index 000000000..6293bc2fa --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.module.blackbird.util; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ModeBuilderBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestReflectionHack +{ + static class SamePackage { } + + private static final Lookup LOOKUP = MethodHandles.lookup(); + + @Test + public void testCanAccessClass() throws Exception + { + Class mode = _packagePrivateFromOtherPackage(); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, int.class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, int[].class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, String.class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, ModeBuilderBase.class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, SamePackage[].class)); + assertFalse(ReflectionHack.canAccessClass(LOOKUP, mode)); + assertFalse(ReflectionHack.canAccessClass(LOOKUP, java.lang.reflect.Array.newInstance(mode, 0).getClass())); + } + + // what Java 8 uses, which can't be picked on newer JDKs + @Test + public void testCanAccessClassWithoutModules() throws Exception + { + Class self = getClass(); + assertTrue(ReflectionHack.canAccessClassWithoutModules(self, String.class)); + assertTrue(ReflectionHack.canAccessClassWithoutModules(self, ModeBuilderBase.class)); + assertTrue(ReflectionHack.canAccessClassWithoutModules(self, SamePackage.class)); + assertFalse(ReflectionHack.canAccessClassWithoutModules(self, _packagePrivateFromOtherPackage())); + } + + private static Class _packagePrivateFromOtherPackage() throws Exception { + return Class.forName("com.fasterxml.jackson.module.blackbird.deser.otherpackage.Mode"); + } +} From b473c69aa4554767c15f8668981b6163793d3d19 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 28 Oct 2026 21:33:55 -0400 Subject: [PATCH 10/34] Leave other value types to the delegate in builder setters --- .../deser/SettableBooleanProperty.java | 4 +- .../blackbird/deser/SettableIntProperty.java | 6 ++- .../blackbird/deser/SettableLongProperty.java | 6 ++- .../deser/SettableObjectProperty.java | 9 +++- .../deser/SettableStringProperty.java | 3 +- .../module/blackbird/deser/TestBuilders.java | 41 +++++++++++++++++++ 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 9ec814216..76c1a4e0d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -81,7 +81,9 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - if (value == null) { + // anything else (null, or another type that reflection may widen or reject) + // is left to the delegate + if (!(value instanceof Boolean)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 09a6a0641..9d8d6e5bc 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -74,11 +74,13 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - if (value == null) { + // anything else (null, or another type that reflection may widen or reject) + // is left to the delegate + if (!(value instanceof Integer)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: - return _setAndReturn(null, instance, ((Number) value).intValue()); + return _setAndReturn(null, instance, ((Integer) value).intValue()); } private Object _setAndReturn(JsonParser p, Object instance, int v) throws IOException { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index cdb59619e..36b6a7a18 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -73,11 +73,13 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - if (value == null) { + // anything else (null, or another type that reflection may widen or reject) + // is left to the delegate + if (!(value instanceof Long)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: - return _setAndReturn(null, instance, ((Number) value).longValue()); + return _setAndReturn(null, instance, ((Long) value).longValue()); } private Object _setAndReturn(JsonParser p, Object instance, long v) throws IOException { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 48ff27c4f..660b9f303 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; final class SettableObjectProperty extends OptimizedSettableBeanProperty @@ -14,6 +15,7 @@ final class SettableObjectProperty private static final long serialVersionUID = 1L; private BiConsumer _optimizedSetter; private BiFunction _optimizedSetterAndReturn; + private final Class _setterValueType; public SettableObjectProperty(SettableBeanProperty src, BiConsumer optimizedSetter, BiFunction optimizedSetterAndReturn) @@ -21,6 +23,9 @@ public SettableObjectProperty(SettableBeanProperty src, BiConsumer type, String name) throws Exception { From 46963b0ffb4a08de62da463e63917ab215310cbe Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Wed, 28 Oct 2026 22:32:38 -0400 Subject: [PATCH 11/34] Match MethodProperty for builder receivers, declaring classes and skipped nulls --- .../deser/BBDeserializerModifier.java | 27 +++- .../deser/OptimizedSettableBeanProperty.java | 10 +- .../deser/SettableBooleanProperty.java | 14 +- .../blackbird/deser/SettableIntProperty.java | 15 +- .../blackbird/deser/SettableLongProperty.java | 15 +- .../deser/SettableObjectProperty.java | 21 +-- .../deser/SettableStringProperty.java | 23 ++-- .../module/blackbird/deser/TestBuilders.java | 128 +++++++++++++++++- .../deser/TestInaccessibleSetterTypes.java | 44 +++++- .../otherpackage/ReplacingBuilderBase.java | 27 ++++ .../deser/otherpackage/ReplacingBuilt.java | 16 +++ .../deser/shadowed/OwnerBuilder.java | 19 +++ .../blackbird/deser/shadowed/OwnerBuilt.java | 13 ++ .../deser/shadowed/owner/OwnerBase.java | 17 +++ .../deser/shadowed/owner/OwnerBridge.java | 3 + 15 files changed, 341 insertions(+), 51 deletions(-) create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilderBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilt.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilder.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilt.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 231b485cb..56474c866 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -172,8 +172,19 @@ private void nextProperty(SettableBeanProperty prop, Class type; if (jdkMember instanceof Method && prop instanceof MethodProperty) { final Method method = (Method) jdkMember; - setter = lookup.unreflect(method); + try { + setter = lookup.unreflect(method); + } catch (IllegalAccessException e) { + // e.g. an inherited protected setter, where the lookup class isn't the + // subclass (as with `CrossLoaderAccess` on some JDKs); reflection can still call it + return; + } type = ((AnnotatedMethod) member).getRawParameterType(0); + // For an inherited protected setter the handle's receiver is narrowed to + // the lookup class, but calling it still names the declaring class + if (!ReflectionHack.canResolveClass(lookup, method.getDeclaringClass())) { + return; + } } else { return; //setter = lookup.unreflectGetter((Field) jdkMember); @@ -192,31 +203,35 @@ private void nextProperty(SettableBeanProperty prop, return; } + final Class receiverType = setter.type().parameterType(0); + if (type.isPrimitive()) { if (type == Integer.TYPE) { ObjIntBiFunction thunk = createThunk(lookup, ObjIntBiFunction.class, int.class, setter); newProps.add(new SettableIntProperty(prop, createSetter(lookup, ObjIntConsumer.class, ObjIntBiFunction.class, - INT_TRAMPOLINE, int.class, setter, thunk), thunk)); + INT_TRAMPOLINE, int.class, setter, thunk), thunk, receiverType)); } else if (type == Long.TYPE) { ObjLongBiFunction thunk = createThunk(lookup, ObjLongBiFunction.class, long.class, setter); newProps.add(new SettableLongProperty(prop, createSetter(lookup, ObjLongConsumer.class, ObjLongBiFunction.class, - LONG_TRAMPOLINE, long.class, setter, thunk), thunk)); + LONG_TRAMPOLINE, long.class, setter, thunk), thunk, receiverType)); } else if (type == Boolean.TYPE) { ObjBooleanBiFunction thunk = createThunk(lookup, ObjBooleanBiFunction.class, boolean.class, setter); newProps.add(new SettableBooleanProperty(prop, createSetter(lookup, ObjBooleanConsumer.class, ObjBooleanBiFunction.class, - BOOLEAN_TRAMPOLINE, boolean.class, setter, thunk), thunk)); + BOOLEAN_TRAMPOLINE, boolean.class, setter, thunk), thunk, receiverType)); } } else { BiFunction thunk = createThunk(lookup, BiFunction.class, Object.class, setter); if (type == String.class) { newProps.add(new SettableStringProperty(prop, - createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), thunk)); + createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), + thunk, receiverType)); } else { newProps.add(new SettableObjectProperty(prop, - createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), thunk)); + createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), + thunk, receiverType)); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 85a0a99c9..3f0dcc0b2 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -32,16 +32,24 @@ abstract class OptimizedSettableBeanProperty _receiverType; + /* /********************************************************************** /* Life-cycle /********************************************************************** */ - protected OptimizedSettableBeanProperty(SettableBeanProperty src) + protected OptimizedSettableBeanProperty(SettableBeanProperty src, Class receiverType) { super(src); _skipNulls = NullsConstantProvider.isSkipper(_nullProvider); + _receiverType = receiverType; } // Base impl of `withName()` fine as-is: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 76c1a4e0d..c80416c9d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -15,16 +15,16 @@ final class SettableBooleanProperty private ObjBooleanBiFunction _optimizedSetterAndReturn; public SettableBooleanProperty(SettableBeanProperty src, ObjBooleanConsumer optimizedSetter, - ObjBooleanBiFunction optimizedSetterAndReturn) + ObjBooleanBiFunction optimizedSetterAndReturn, Class receiverType) { - super(src); + super(src, receiverType); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableBooleanProperty(del, _optimizedSetter, _optimizedSetterAndReturn); + return new SettableBooleanProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); } /* @@ -56,7 +56,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if (_optimizedSetterAndReturn != null) { + if ((_optimizedSetterAndReturn != null) && _receiverType.isInstance(instance)) { JsonToken t = p.currentToken(); if (t == JsonToken.VALUE_TRUE) { return _setAndReturn(p, instance, true); @@ -81,9 +81,9 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // anything else (null, or another type that reflection may widen or reject) - // is left to the delegate - if (!(value instanceof Boolean)) { + // anything else (null, or another type that reflection may widen or reject), + // or an instance the generated setter can't take, is left to the delegate + if (!(value instanceof Boolean) || !_receiverType.isInstance(instance)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 9d8d6e5bc..49b232d61 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -16,16 +16,16 @@ final class SettableIntProperty private ObjIntBiFunction _optimizedSetterAndReturn; public SettableIntProperty(SettableBeanProperty src, ObjIntConsumer optimizedSetter, - ObjIntBiFunction optimizedSetterAndReturn) + ObjIntBiFunction optimizedSetterAndReturn, Class receiverType) { - super(src); + super(src, receiverType); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableIntProperty(del, _optimizedSetter, _optimizedSetterAndReturn); + return new SettableIntProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); } /* @@ -55,7 +55,8 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if ((_optimizedSetterAndReturn != null) && p.isExpectedNumberIntToken()) { + if ((_optimizedSetterAndReturn != null) && _receiverType.isInstance(instance) + && p.isExpectedNumberIntToken()) { return _setAndReturn(p, instance, p.getIntValue()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); @@ -74,9 +75,9 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // anything else (null, or another type that reflection may widen or reject) - // is left to the delegate - if (!(value instanceof Integer)) { + // anything else (null, or another type that reflection may widen or reject), + // or an instance the generated setter can't take, is left to the delegate + if (!(value instanceof Integer) || !_receiverType.isInstance(instance)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 36b6a7a18..754bd80d1 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -16,16 +16,16 @@ final class SettableLongProperty private ObjLongBiFunction _optimizedSetterAndReturn; public SettableLongProperty(SettableBeanProperty src, ObjLongConsumer optimizedSetter, - ObjLongBiFunction optimizedSetterAndReturn) + ObjLongBiFunction optimizedSetterAndReturn, Class receiverType) { - super(src); + super(src, receiverType); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableLongProperty(del, _optimizedSetter, _optimizedSetterAndReturn); + return new SettableLongProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); } /* @@ -54,7 +54,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if ((_optimizedSetterAndReturn != null) && p.isExpectedNumberIntToken()) { + if ((_optimizedSetterAndReturn != null) && _receiverType.isInstance(instance) + && p.isExpectedNumberIntToken()) { return _setAndReturn(p, instance, p.getLongValue()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); @@ -73,9 +74,9 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // anything else (null, or another type that reflection may widen or reject) - // is left to the delegate - if (!(value instanceof Long)) { + // anything else (null, or another type that reflection may widen or reject), + // or an instance the generated setter can't take, is left to the delegate + if (!(value instanceof Long) || !_receiverType.isInstance(instance)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 660b9f303..234072e73 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -18,9 +18,9 @@ final class SettableObjectProperty private final Class _setterValueType; public SettableObjectProperty(SettableBeanProperty src, BiConsumer optimizedSetter, - BiFunction optimizedSetterAndReturn) + BiFunction optimizedSetterAndReturn, Class receiverType) { - super(src); + super(src, receiverType); this._optimizedSetter = optimizedSetter; this._optimizedSetterAndReturn = optimizedSetterAndReturn; _setterValueType = (src.getMember() instanceof AnnotatedMethod) @@ -30,7 +30,7 @@ public SettableObjectProperty(SettableBeanProperty src, BiConsumer _optimizedSetterAndReturn; public SettableStringProperty(SettableBeanProperty src, BiConsumer optimizedSetter, - BiFunction optimizedSetterAndReturn) + BiFunction optimizedSetterAndReturn, Class receiverType) { - super(src); + super(src, receiverType); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableStringProperty(del, _optimizedSetter, _optimizedSetterAndReturn); + return new SettableStringProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); } /* @@ -48,7 +48,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object @Override public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { - if ((_optimizedSetterAndReturn != null) && p.hasToken(JsonToken.VALUE_STRING)) { + if ((_optimizedSetterAndReturn != null) && _receiverType.isInstance(instance) + && p.hasToken(JsonToken.VALUE_STRING)) { return _setAndReturn(p, instance, p.getText()); } return delegate.deserializeSetAndReturn(p, ctxt, instance); @@ -65,17 +66,21 @@ public void set(Object bean, Object value) throws IOException { @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // a value of another type is left to the delegate, which reports it - if ((_optimizedSetterAndReturn == null) || ((value != null) && !(value instanceof String))) { + // a value of another type, or an instance the generated setter can't take, + // is left to the delegate, which reports it + if ((_optimizedSetterAndReturn == null) || !_receiverType.isInstance(instance) + || ((value != null) && !(value instanceof String))) { return delegate.setAndReturn(instance, value); } + // as in `MethodProperty`, only here: `deserializeSetAndReturn` already skipped + // the nulls it should + if ((value == null) && _skipNulls) { + return instance; + } return _setAndReturn(null, instance, value); } private Object _setAndReturn(JsonParser p, Object instance, Object value) throws IOException { - if ((value == null) && _skipNulls) { - return instance; - } try { Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index c1731188f..8e0705b53 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -27,6 +28,8 @@ import com.fasterxml.jackson.databind.deser.impl.MethodProperty; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilderBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilt; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -359,6 +362,78 @@ public HideableBuilder withStr(String v) { @JsonDeserialize(builder=HideableBuilder.class) static class HideableBuilt { } + // Inherits protected `withX`, whose generated setter only takes this class, but + // `withFirst` replaces the builder with an instance of the base class + public static final class ReplacingBuilder extends ReplacingBuilderBase { } + + // `withFirst` returns something that isn't a builder at all + static final class UnrelatedBuilder + { + public Object withFirst(int v) { + return "not a builder"; + } + + public UnrelatedBuilder withX(int v) { + return this; + } + + public UnrelatedBuilt build() { + return new UnrelatedBuilt(); + } + } + + @JsonDeserialize(builder=UnrelatedBuilder.class) + static class UnrelatedBuilt { } + + static final class NullCreatorBuilder + { + @JsonCreator + public static NullCreatorBuilder create() { + return null; + } + + public NullCreatorBuilder withX(int v) { + return this; + } + + public NullCreatorBuilt build() { + return new NullCreatorBuilt(); + } + } + + @JsonDeserialize(builder=NullCreatorBuilder.class) + static class NullCreatorBuilt { } + + // An unknown type id gives null, which plain Jackson passes to the setter even + // with `Nulls.SKIP` + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, defaultImpl=Void.class) + static class Item { + public String name; + } + + static int itemCalls; + + static final class ItemBuilder + { + @JsonSetter(nulls=Nulls.SKIP) + public ItemBuilder withFluent(Item i) { + itemCalls++; + return this; + } + + @JsonSetter(nulls=Nulls.SKIP) + public void withVoid(Item i) { + itemCalls++; + } + + public ItemBuilt build() { + return new ItemBuilt(); + } + } + + @JsonDeserialize(builder=ItemBuilder.class) + static class ItemBuilt { } + static class Values { final String str; final List list; @@ -468,7 +543,8 @@ public void accept(Object bean, String v) { public Object apply(Object bean, Object v) { throw new IllegalAccessError("simulated"); } - }); + }, + CopyingBuilder.class); CopyingBuilder result = (CopyingBuilder) prop.setAndReturn(new CopyingBuilder(), "foo"); assertEquals("foo", result.str); @@ -601,6 +677,56 @@ public void testHiddenClassSetterFailureIsNotRetried() throws Exception } } + @Test + public void testReplacedBuilderOfBaseClass() throws Exception + { + String json = a2q("{'first':1,'x':2}"); + ReplacingBuilt expected = newVanillaJSONMapper().readValue(json, ReplacingBuilt.class); + ReplacingBuilt actual = MAPPER.readValue(json, ReplacingBuilt.class); + assertEquals(1, expected.first); + assertEquals(2, expected.x); + assertEquals(expected.first, actual.first); + assertEquals(expected.x, actual.x); + } + + // A builder that isn't an instance of the setter's class (or is null) must fail + // as reflection does, not as a setter failure from the generated cast + @Test + public void testUnusableBuilderInstanceMatchesVanilla() throws Exception + { + assertTrue(_findProperty(MAPPER, UnrelatedBuilt.class, "x") instanceof OptimizedSettableBeanProperty); + assertTrue(_findProperty(MAPPER, NullCreatorBuilt.class, "x") instanceof OptimizedSettableBeanProperty); + for (boolean wrap : new boolean[] { true, false }) { + ObjectMapper vanilla = JsonMapper.builder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + ObjectMapper blackbird = mapperBuilder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + for (Class type : new Class[] { UnrelatedBuilt.class, NullCreatorBuilt.class }) { + String json = a2q("{'first':1,'x':2}"); + Throwable expected = assertThrows(Throwable.class, () -> vanilla.readValue(json, type)); + Throwable actual = assertThrows(Throwable.class, () -> blackbird.readValue(json, type)); + _assertSameFailure("wrap=" + wrap + ", " + type.getSimpleName() + ", " + expected, + expected, actual); + } + } + } + + @Test + public void testPolymorphicNullMatchesVanilla() throws Exception + { + assertTrue(_findProperty(MAPPER, ItemBuilt.class, "fluent") instanceof OptimizedSettableBeanProperty); + assertTrue(_findProperty(MAPPER, ItemBuilt.class, "void") instanceof OptimizedSettableBeanProperty); + for (String name : new String[] { "fluent", "void" }) { + String json = a2q("{'" + name + "':{'@type':'Unknown'}}"); + itemCalls = 0; + newVanillaJSONMapper().readValue(json, ItemBuilt.class); + int expected = itemCalls; + itemCalls = 0; + MAPPER.readValue(json, ItemBuilt.class); + assertEquals(expected, itemCalls, name); + } + } + // `setAndReturn` can be given values its property's deserializer didn't produce, // such as an object id from a generator. Reflection widens some and rejects // others, and the generated setters must not cast or truncate them instead. diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java index 4fb6e22f6..8924d2cb5 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -15,6 +15,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.module.blackbird.BlackbirdModule; import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; @@ -32,6 +34,9 @@ import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBuilder; import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBuilderBase; import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBuilt; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.OwnerBuilder; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.OwnerBuilt; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.owner.OwnerBase; import static org.junit.jupiter.api.Assertions.*; @@ -137,6 +142,30 @@ public void testShadowedReturnType() throws Exception assertEquals(MethodProperty.class, _findProperty(built, "level").getClass()); } + // And for the declaring class of an inherited protected setter, which isn't in the + // handle's type (its receiver is narrowed to the builder), but still named by the call + @Test + public void testShadowedDeclaringClass() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(OwnerBase.class, OwnerBuilder.class, OwnerBuilt.class)); + Class built = Class.forName(OwnerBuilt.class.getName(), true, loader); + Class builder = Class.forName(OwnerBuilder.class.getName(), true, loader); + assertNotSame(OwnerBuilt.class, built); + assertNotSame(OwnerBase.class, Class.forName(OwnerBase.class.getName(), false, loader)); + // a lookup in the builder itself, which the protected setter needs + MethodHandles.Lookup builderLookup = (MethodHandles.Lookup) builder.getMethod("lookup").invoke(null); + ObjectMapper mapper = JsonMapper.builder() + .addModule(new BlackbirdModule(c -> (c == builder) ? builderLookup : MethodHandles.lookup())) + .build(); + + OwnerBase.setterCalls = 0; + Object result = mapper.readValue(a2q("{'value':7}"), built); + assertEquals(7, built.getField("value").get(result)); + assertEquals(1, OwnerBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(mapper, built, "value").getClass()); + } + // Setup skips this accessor, so build it the same way here to check that the // access error the JVM really raises (from a hidden frame on Java 9+) is // recognized as the accessor's, and falls back without running the setter twice @@ -160,7 +189,8 @@ public void testRealAccessorFailureFallsBack() throws Throwable .createInstance(vanilla.getDeserializationConfig(), null, null); BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( vanilla.constructType(ModeBuilt.class)); - SettableObjectProperty prop = new SettableObjectProperty(deser.findProperty("mode"), unused, thunk); + SettableObjectProperty prop = new SettableObjectProperty(deser.findProperty("mode"), unused, thunk, + setter.type().parameterType(0)); Object fast = mode.getEnumConstants()[0]; for (int i = 0; i < 2; i++) { @@ -183,10 +213,16 @@ private void _assertOnlyModeUsesReflection(Class type) throws Exception private SettableBeanProperty _findProperty(Class type, String name) throws Exception { - DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) MAPPER.getDeserializationContext()) - .createInstance(MAPPER.getDeserializationConfig(), null, null); + return _findProperty(MAPPER, type, name); + } + + private static SettableBeanProperty _findProperty(ObjectMapper mapper, Class type, String name) + throws Exception + { + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) mapper.getDeserializationContext()) + .createInstance(mapper.getDeserializationConfig(), null, null); BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( - MAPPER.constructType(type)); + mapper.constructType(type)); return deser.findProperty(name); } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilderBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilderBase.java new file mode 100644 index 000000000..04e5829eb --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilderBase.java @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +// `withFirst` returns a new instance of this class, not of the subclass the builder +// started as, and reflection can still call the protected `withX` on it +public class ReplacingBuilderBase +{ + protected int first; + protected int x; + + public ReplacingBuilderBase withFirst(int v) { + ReplacingBuilderBase b = new ReplacingBuilderBase(); + b.first = v; + return b; + } + + @JsonProperty("x") + protected ReplacingBuilderBase withX(int v) { + x = v; + return this; + } + + public ReplacingBuilt build() { + return new ReplacingBuilt(first, x); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilt.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilt.java new file mode 100644 index 000000000..6a8a943d7 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ReplacingBuilt.java @@ -0,0 +1,16 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.module.blackbird.deser.TestBuilders; + +@JsonDeserialize(builder=TestBuilders.ReplacingBuilder.class) +public class ReplacingBuilt +{ + public final int first; + public final int x; + + ReplacingBuilt(int first, int x) { + this.first = first; + this.x = x; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilder.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilder.java new file mode 100644 index 000000000..eae0d079c --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilder.java @@ -0,0 +1,19 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.module.blackbird.deser.shadowed.owner.OwnerBridge; + +// in another package than the protected setter's declaring class, so its handle's +// receiver is narrowed to this class +public final class OwnerBuilder extends OwnerBridge +{ + // so Blackbird can be given a lookup in this class, as for protected setters it needs + public static MethodHandles.Lookup lookup() { + return MethodHandles.lookup(); + } + + public OwnerBuilt build() { + return new OwnerBuilt(value); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilt.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilt.java new file mode 100644 index 000000000..b5618b037 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBuilt.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder=OwnerBuilder.class) +public class OwnerBuilt +{ + public final int value; + + OwnerBuilt(int value) { + this.value = value; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java new file mode 100644 index 000000000..f2d8f13c0 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed.owner; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OwnerBase +{ + public static int setterCalls; + + protected int value; + + @JsonProperty("value") + protected Object withValue(int v) { + setterCalls++; + value = v; + return this; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java new file mode 100644 index 000000000..ec121e379 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed.owner; + +public class OwnerBridge extends OwnerBase { } From 59706d7480d95f2aa1aa7683326e60d5c82e8926 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 07:23:38 -0400 Subject: [PATCH 12/34] Handle other value types, null text and failing stack traces like MethodProperty --- .../deser/OptimizedSettableBeanProperty.java | 37 +++- .../deser/SettableObjectProperty.java | 5 + .../deser/SettableStringProperty.java | 7 +- .../module/blackbird/deser/TestBuilders.java | 203 ++++++++++++++++-- 4 files changed, 235 insertions(+), 17 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 3f0dcc0b2..229d02dd1 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.util.logging.Level; import java.util.logging.Logger; @@ -147,7 +149,7 @@ protected Object _reportProblemAndReturn(JsonParser p, Object accessor, Object b { if (_isAccessorAccessFailure(accessor, bean, e)) { _logAccessProblem(bean, e); - return delegate.setAndReturn(bean, value); + return _reflectiveSetAndReturn(p, bean, value); } // `Method.invoke` wraps whatever the setter throws, and `MethodProperty` // translates that wrapper; doing the same keeps type, message, cause and path @@ -155,6 +157,28 @@ protected Object _reportProblemAndReturn(JsonParser p, Object accessor, Object b return null; // never gets here } + /** + * Calls the setter the way {@code MethodProperty.deserializeSetAndReturn} does once + * it has the value, so a failure is reported the same way, with the parser's + * location if {@code p} isn't null. For a value or builder the generated setter + * can't take, once the value has been read. + */ + protected Object _reflectiveSetAndReturn(JsonParser p, Object instance, Object value) + throws IOException + { + Member setter = getMember().getMember(); + if (!(setter instanceof Method)) { + return delegate.setAndReturn(instance, value); + } + try { + Object result = ((Method) setter).invoke(instance, value); + return (result == null) ? instance : result; + } catch (Exception e) { + _throwAsIOE(p, e, value); + return null; // never gets here + } + } + // Retrying is only safe if the setter never ran, so require positive evidence: // the JVM raises an access failure in the accessor's own frame, which is then // on top of the stack (or, where generated frames are hidden from stack traces, @@ -164,10 +188,17 @@ private boolean _isAccessorAccessFailure(Object accessor, Object bean, Throwable if (!(e instanceof IllegalAccessError) && !(e instanceof SecurityException)) { return false; } - StackTraceElement[] trace = e.getStackTrace(); + StackTraceElement[] trace; + try { + trace = e.getStackTrace(); + } catch (RuntimeException traceFailure) { + // a subclass can override it; that is no evidence either, and mustn't + // replace the setter's failure + return false; + } // No trace (stack traces disabled, or a stackless throwable) is no evidence; // setup already skips the accessors known to fail this way - if (trace.length == 0) { + if ((trace == null) || (trace.length == 0) || (trace[0] == null)) { return false; } String top = trace[0].getClassName(); diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 234072e73..0608eb283 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -89,6 +89,11 @@ public Object deserializeSetAndReturn(JsonParser p, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } + // a deserializer can give a value of another type (e.g. from a creator declared + // to return `Object`), which the generated setter can't take + if ((value != null) && !_setterValueType.isInstance(value)) { + return _reflectiveSetAndReturn(p, instance, value); + } return _setAndReturn(p, instance, value); } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index f0522bded..53a66f6c9 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -50,7 +50,12 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, { if ((_optimizedSetterAndReturn != null) && _receiverType.isInstance(instance) && p.hasToken(JsonToken.VALUE_STRING)) { - return _setAndReturn(p, instance, p.getText()); + String text = p.getText(); + // some parsers give null here (e.g. for a `TextNode(null)`), which needs + // the delegate's null handling + if (text != null) { + return _setAndReturn(p, instance, text); + } } return delegate.deserializeSetAndReturn(p, ctxt, instance); } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 8e0705b53..d70e85d9c 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,6 +28,9 @@ import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.impl.MethodProperty; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilderBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilt; @@ -373,9 +377,11 @@ public Object withFirst(int v) { return "not a builder"; } - public UnrelatedBuilder withX(int v) { - return this; - } + public UnrelatedBuilder withX(int v) { return this; } + public UnrelatedBuilder withL(long v) { return this; } + public UnrelatedBuilder withB(boolean v) { return this; } + public UnrelatedBuilder withS(String v) { return this; } + public UnrelatedBuilder withList(List v) { return this; } public UnrelatedBuilt build() { return new UnrelatedBuilt(); @@ -392,9 +398,11 @@ public static NullCreatorBuilder create() { return null; } - public NullCreatorBuilder withX(int v) { - return this; - } + public NullCreatorBuilder withX(int v) { return this; } + public NullCreatorBuilder withL(long v) { return this; } + public NullCreatorBuilder withB(boolean v) { return this; } + public NullCreatorBuilder withS(String v) { return this; } + public NullCreatorBuilder withList(List v) { return this; } public NullCreatorBuilt build() { return new NullCreatorBuilt(); @@ -404,6 +412,93 @@ public NullCreatorBuilt build() { @JsonDeserialize(builder=NullCreatorBuilder.class) static class NullCreatorBuilt { } + // Its creator gives a `String`, not a `Child`, which databind accepts + static class Child + { + public int foo; + + @JsonCreator + public static Object create() { + return "not a Child"; + } + } + + static final class ChildBuilder + { + public ChildBuilder withChild(Child c) { + return this; + } + + public ChildBuilt build() { + return new ChildBuilt(); + } + } + + @JsonDeserialize(builder=ChildBuilder.class) + static class ChildBuilt { } + + // Overrides `getStackTrace`, which `MethodProperty` never calls + @SuppressWarnings("serial") + static class TraceFailingException extends SecurityException + { + TraceFailingException() { + super("thrown by setter"); + } + + @Override + public StackTraceElement[] getStackTrace() { + throw new IllegalStateException("no trace for you"); + } + } + + static final class TraceFailingBuilder + { + public TraceFailingBuilder withX(int v) { + throw new TraceFailingException(); + } + + public TraceFailingBuilt build() { + return new TraceFailingBuilt(); + } + } + + @JsonDeserialize(builder=TraceFailingBuilder.class) + static class TraceFailingBuilt { } + + static final class NullTextBuilder + { + String skipped = "default"; + String plain = "default"; + int calls; + + @JsonSetter(nulls=Nulls.SKIP) + public NullTextBuilder withSkipped(String v) { + calls++; + skipped = v; + return this; + } + + public NullTextBuilder withPlain(String v) { + calls++; + plain = v; + return this; + } + + public NullTextBuilt build() { + return new NullTextBuilt(skipped + "," + plain + "," + calls); + } + } + + @JsonDeserialize(builder=NullTextBuilder.class) + static class NullTextBuilt + { + final String result; + + NullTextBuilt(String result) { + this.result = result; + } + } + // An unknown type id gives null, which plain Jackson passes to the setter even // with `Nulls.SKIP` @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, defaultImpl=Void.class) @@ -452,9 +547,12 @@ static class Values { } } + // through `MethodProperty`, or Blackbird's own reflective call for what its + // generated setters can't take static int reflectiveCall() { for (StackTraceElement elem : new Throwable().getStackTrace()) { - if (elem.getClassName().equals(MethodProperty.class.getName())) { + if (elem.getClassName().equals(MethodProperty.class.getName()) + || elem.getMethodName().equals("_reflectiveSetAndReturn")) { return 1; } } @@ -701,16 +799,95 @@ public void testUnusableBuilderInstanceMatchesVanilla() throws Exception .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); ObjectMapper blackbird = mapperBuilder() .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); - for (Class type : new Class[] { UnrelatedBuilt.class, NullCreatorBuilt.class }) { - String json = a2q("{'first':1,'x':2}"); - Throwable expected = assertThrows(Throwable.class, () -> vanilla.readValue(json, type)); - Throwable actual = assertThrows(Throwable.class, () -> blackbird.readValue(json, type)); - _assertSameFailure("wrap=" + wrap + ", " + type.getSimpleName() + ", " + expected, - expected, actual); + for (String value : new String[] { "'x':2", "'l':2", "'b':true", "'s':'a'", "'list':['a']" }) { + // the unrelated builder comes from `withFirst`, the null one from its creator + for (String json : new String[] { a2q("{'first':1," + value + "}"), a2q("{" + value + "}") }) { + Class type = json.contains("first") ? UnrelatedBuilt.class : NullCreatorBuilt.class; + Throwable expected = assertThrows(Throwable.class, () -> vanilla.readValue(json, type)); + Throwable actual = assertThrows(Throwable.class, () -> blackbird.readValue(json, type)); + String desc = "wrap=" + wrap + ", " + type.getSimpleName() + ", " + json + ", " + expected; + // and not an unknown property, or some other failure before the setter + assertFalse(expected instanceof com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException, + desc); + _assertSameFailure(desc, expected, actual); + } } } } + // If the generated setter fails to access it and the setter then fails through + // reflection, the error still has the JSON location + @Test + public void testAccessFailureFallbackKeepsLocation() throws Exception + { + ObjectMapper vanilla = newVanillaJSONMapper(); + BeanDeserializerBase deser = (BeanDeserializerBase) ((DefaultDeserializationContext) vanilla.getDeserializationContext()) + .createInstance(vanilla.getDeserializationConfig(), null, null) + .findRootValueDeserializer(vanilla.constructType(ThrowingBuilt.class)); + SettableStringProperty prop = new SettableStringProperty(deser.findProperty("str"), + new BiConsumer() { + @Override + public void accept(Object bean, String v) { + throw new IllegalAccessError("simulated"); + } + }, + new BiFunction() { + @Override + public Object apply(Object bean, Object v) { + throw new IllegalAccessError("simulated"); + } + }, + ThrowingBuilder.class); + toThrow = () -> new IllegalStateException("thrown by setter"); + setterCalls = 0; + try (JsonParser p = vanilla.createParser(a2q("{'str':'foo'}"))) { + p.nextToken(); + p.nextToken(); + p.nextToken(); + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) vanilla.getDeserializationContext()) + .createInstance(vanilla.getDeserializationConfig(), p, null); + JsonMappingException e = assertThrows(JsonMappingException.class, + () -> prop.deserializeSetAndReturn(p, ctxt, new ThrowingBuilder())); + assertEquals("thrown by setter", e.getOriginalMessage()); + assertNotNull(e.getLocation()); + } + assertEquals(1, setterCalls); + } + + @Test + public void testValueOfAnotherTypeMatchesVanilla() throws Exception + { + assertTrue(_findProperty(MAPPER, ChildBuilt.class, "child") instanceof OptimizedSettableBeanProperty); + String json = a2q("{'child':{}}"); + Throwable expected = assertThrows(Throwable.class, () -> newVanillaJSONMapper().readValue(json, ChildBuilt.class)); + Throwable actual = assertThrows(Throwable.class, () -> MAPPER.readValue(json, ChildBuilt.class)); + _assertSameFailure(String.valueOf(expected), expected, actual); + } + + @Test + public void testFailingStackTraceKeepsSetterFailure() throws Exception + { + String json = a2q("{'x':1}"); + Throwable expected = assertThrows(Throwable.class, + () -> newVanillaJSONMapper().readValue(json, TraceFailingBuilt.class)); + Throwable actual = assertThrows(Throwable.class, () -> MAPPER.readValue(json, TraceFailingBuilt.class)); + assertEquals(TraceFailingException.class, actual.getCause().getClass()); + _assertSameFailure(String.valueOf(expected), expected, actual); + } + + // `TextNode(null)` reads as a VALUE_STRING whose text is null + @Test + public void testNullTextMatchesVanilla() throws Exception + { + assertTrue(_findProperty(MAPPER, NullTextBuilt.class, "skipped") instanceof OptimizedSettableBeanProperty); + ObjectNode tree = JsonNodeFactory.instance.objectNode(); + tree.set("skipped", new TextNode(null)); + tree.set("plain", new TextNode(null)); + String expected = newVanillaJSONMapper().treeToValue(tree, NullTextBuilt.class).result; + assertEquals("default,null,1", expected); + assertEquals(expected, MAPPER.treeToValue(tree, NullTextBuilt.class).result); + } + @Test public void testPolymorphicNullMatchesVanilla() throws Exception { From 526e2df80de9cedb2b531f2a2aaf08cca962fe95 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 07:26:08 -0400 Subject: [PATCH 13/34] Keep builder setters on reflection when access modifier overrides are off --- .../deser/BBDeserializerModifier.java | 24 ++++++--- .../module/blackbird/deser/TestBuilders.java | 53 +++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 56474c866..49ddfd3a5 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -129,7 +129,7 @@ protected List> findOptimizableProperties( // Ok, then, find any properties for which we could generate accessors while (propIterator.hasNext()) { try { - nextProperty(propIterator.next(), lookup, newProps); + nextProperty(propIterator.next(), lookup, config, newProps); } catch (Throwable e) { if (e instanceof Error) { throw (Error) e; @@ -145,7 +145,7 @@ protected List> findOptimizableProperties( @SuppressWarnings("unchecked") private void nextProperty(SettableBeanProperty prop, - Lookup lookup, + Lookup lookup, DeserializationConfig config, ArrayList> newProps) throws Throwable { AnnotatedMember member = prop.getMember(); @@ -170,6 +170,7 @@ private void nextProperty(SettableBeanProperty prop, MethodHandle setter; Class type; + boolean optimizeBuilderSetter; if (jdkMember instanceof Method && prop instanceof MethodProperty) { final Method method = (Method) jdkMember; try { @@ -185,6 +186,12 @@ private void nextProperty(SettableBeanProperty prop, if (!ReflectionHack.canResolveClass(lookup, method.getDeclaringClass())) { return; } + // Builder setters used to go through reflection, which without + // CAN_OVERRIDE_ACCESS_MODIFIERS can only call public setters of public + // classes; the generated one must not call any others + optimizeBuilderSetter = config.canOverrideAccessModifiers() + || (Modifier.isPublic(method.getModifiers()) + && Modifier.isPublic(method.getDeclaringClass().getModifiers())); } else { return; //setter = lookup.unreflectGetter((Field) jdkMember); @@ -210,28 +217,31 @@ private void nextProperty(SettableBeanProperty prop, ObjIntBiFunction thunk = createThunk(lookup, ObjIntBiFunction.class, int.class, setter); newProps.add(new SettableIntProperty(prop, createSetter(lookup, ObjIntConsumer.class, ObjIntBiFunction.class, - INT_TRAMPOLINE, int.class, setter, thunk), thunk, receiverType)); + INT_TRAMPOLINE, int.class, setter, thunk), + optimizeBuilderSetter ? thunk : null, receiverType)); } else if (type == Long.TYPE) { ObjLongBiFunction thunk = createThunk(lookup, ObjLongBiFunction.class, long.class, setter); newProps.add(new SettableLongProperty(prop, createSetter(lookup, ObjLongConsumer.class, ObjLongBiFunction.class, - LONG_TRAMPOLINE, long.class, setter, thunk), thunk, receiverType)); + LONG_TRAMPOLINE, long.class, setter, thunk), + optimizeBuilderSetter ? thunk : null, receiverType)); } else if (type == Boolean.TYPE) { ObjBooleanBiFunction thunk = createThunk(lookup, ObjBooleanBiFunction.class, boolean.class, setter); newProps.add(new SettableBooleanProperty(prop, createSetter(lookup, ObjBooleanConsumer.class, ObjBooleanBiFunction.class, - BOOLEAN_TRAMPOLINE, boolean.class, setter, thunk), thunk, receiverType)); + BOOLEAN_TRAMPOLINE, boolean.class, setter, thunk), + optimizeBuilderSetter ? thunk : null, receiverType)); } } else { BiFunction thunk = createThunk(lookup, BiFunction.class, Object.class, setter); if (type == String.class) { newProps.add(new SettableStringProperty(prop, createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), - thunk, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType)); } else { newProps.add(new SettableObjectProperty(prop, createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), - thunk, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType)); } } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index d70e85d9c..9bb6151cd 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -15,12 +15,14 @@ import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; @@ -412,6 +414,35 @@ public NullCreatorBuilt build() { @JsonDeserialize(builder=NullCreatorBuilder.class) static class NullCreatorBuilt { } + // Reflection can't call the protected setter without CAN_OVERRIDE_ACCESS_MODIFIERS + static int protectedCalls; + + public static final class ProtectedSetterBuilder + { + int value; + + @JsonProperty("value") + protected ProtectedSetterBuilder withValue(int v) { + protectedCalls++; + value = v; + return this; + } + + public ProtectedSetterBuilt build() { + return new ProtectedSetterBuilt(value); + } + } + + @JsonDeserialize(builder=ProtectedSetterBuilder.class) + public static final class ProtectedSetterBuilt + { + public final int value; + + ProtectedSetterBuilt(int value) { + this.value = value; + } + } + // Its creator gives a `String`, not a `Child`, which databind accepts static class Child { @@ -854,6 +885,28 @@ public Object apply(Object bean, Object v) { assertEquals(1, setterCalls); } + @Test + public void testAccessModifierOverridesMatchVanilla() throws Exception + { + String json = a2q("{'value':42}"); + for (boolean canOverride : new boolean[] { true, false }) { + ObjectMapper vanilla = JsonMapper.builder() + .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride).build(); + ObjectMapper blackbird = mapperBuilder() + .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride).build(); + if (canOverride) { + assertEquals(42, vanilla.readValue(json, ProtectedSetterBuilt.class).value); + assertEquals(42, blackbird.readValue(json, ProtectedSetterBuilt.class).value); + continue; + } + protectedCalls = 0; + Throwable expected = assertThrows(Throwable.class, () -> vanilla.readValue(json, ProtectedSetterBuilt.class)); + Throwable actual = assertThrows(Throwable.class, () -> blackbird.readValue(json, ProtectedSetterBuilt.class)); + _assertSameFailure(String.valueOf(expected), expected, actual); + assertEquals(0, protectedCalls); + } + } + @Test public void testValueOfAnotherTypeMatchesVanilla() throws Exception { From c231b64fcf1ae3e99b1a847ca6fc213c227f52d5 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 08:24:28 -0400 Subject: [PATCH 14/34] Don't load setter return types at setup, and tolerate failing loaders --- .../deser/BBDeserializerModifier.java | 5 +- .../module/blackbird/util/ReflectionHack.java | 36 ++++++++++-- .../module/blackbird/deser/TestBuilders.java | 55 +++++++++++++++++++ .../deser/TestInaccessibleSetterTypes.java | 49 ++++++++++++++++- .../otherpackage/HiddenSetterBuilderBase.java | 16 ++++++ 5 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBuilderBase.java diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 49ddfd3a5..8d0f9ad24 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -205,8 +205,9 @@ private void nextProperty(SettableBeanProperty prop, } } // Calling the setter also names its return type, which nothing casts to, but - // the JVM still requires to be the same class from the accessor's loader - if (!ReflectionHack.canResolveClass(lookup, setter.type().returnType())) { + // the JVM still requires to be the same class from the accessor's loader. + // The accessor never loads it, so neither may this check. + if (!ReflectionHack.isSameClassWithoutLoading(lookup, setter.type().returnType())) { return; } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java index b6fb29397..3f18f10ff 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java @@ -53,9 +53,9 @@ public static boolean canAccessClass(Lookup lookup, Class type) { } /** - * Like {@link #canAccessClass}, for a type that code only names, as in the - * descriptor of a method it calls: it must resolve to the same class, but - * need not be accessible. + * Like {@link #canAccessClass}, for a type that code refers to and so resolves + * (as the owner of a method it calls), but need not be able to access: it must + * resolve to the same class. */ public static boolean canResolveClass(Lookup lookup, Class type) { type = elementType(type); @@ -69,6 +69,25 @@ public static boolean canResolveClass(Lookup lookup, Class type) { } } + /** + * Whether {@code type}, which code defined with {@code lookup} only names in a + * descriptor (as a called method's return type) and so never loads itself, is + * known to be the same class from the lookup class's loader. Unlike + * {@link #canResolveClass} this doesn't ask that loader, which could make it + * define its own copy and break code that relies on it never doing so. + */ + public static boolean isSameClassWithoutLoading(Lookup lookup, Class type) { + type = elementType(type); + if (type.isPrimitive() || type.getName().startsWith("java.")) { + return true; + } + try { + return type.getClassLoader() == lookup.lookupClass().getClassLoader(); + } catch (SecurityException e) { + return false; + } + } + private static Class elementType(Class type) { while (type.isArray()) { type = type.getComponentType(); @@ -80,9 +99,16 @@ private static Class elementType(Class type) { // which may find a different class of the same name (e.g. a child-first loader // with its own copy) or none at all private static boolean resolvesTo(Lookup lookup, Class type) { + ClassLoader loader = lookup.lookupClass().getClassLoader(); + // a class its loader defined is what that loader resolves its name to, + // without asking it to load anything + if (type.getClassLoader() == loader) { + return true; + } try { - return Class.forName(type.getName(), false, lookup.lookupClass().getClassLoader()) == type; - } catch (ClassNotFoundException | LinkageError e) { + return Class.forName(type.getName(), false, loader) == type; + } catch (ClassNotFoundException | LinkageError | RuntimeException e) { + // including whatever else a misbehaving or closed loader throws return false; } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 9bb6151cd..b2a3aab75 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -34,6 +34,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.HiddenSetterBuilderBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilderBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilt; @@ -414,6 +415,25 @@ public NullCreatorBuilt build() { @JsonDeserialize(builder=NullCreatorBuilder.class) static class NullCreatorBuilt { } + // Inherits a package-private setter from another package, which the generated + // setter can't reach (`unreflect` refuses it) but reflection can + public static final class HiddenSetterBuilder extends HiddenSetterBuilderBase + { + public HiddenSetterBuilt build() { + return new HiddenSetterBuilt(y); + } + } + + @JsonDeserialize(builder=HiddenSetterBuilder.class) + static class HiddenSetterBuilt + { + final int y; + + HiddenSetterBuilt(int y) { + this.y = y; + } + } + // Reflection can't call the protected setter without CAN_OVERRIDE_ACCESS_MODIFIERS static int protectedCalls; @@ -885,6 +905,41 @@ public Object apply(Object bean, Object v) { assertEquals(1, setterCalls); } + @Test + public void testInheritedPackagePrivateSetter() throws Exception + { + assertEquals(3, newVanillaJSONMapper().readValue(a2q("{'y':3}"), HiddenSetterBuilt.class).y); + assertEquals(3, MAPPER.readValue(a2q("{'y':3}"), HiddenSetterBuilt.class).y); + } + + // As `deserializeSetAndReturn` does, `setAndReturn` leaves a builder the generated + // setter can't take to reflection, and skips nulls + @Test + public void testSetAndReturnWithUnusableBuilder() throws Exception + { + ObjectMapper vanilla = newVanillaJSONMapper(); + Object[][] cases = { { "i", 1 }, { "l", 1L }, { "b", true }, { "str", "x" }, { "list", Arrays.asList("x") } }; + for (Object[] c : cases) { + String name = (String) c[0]; + for (Object instance : new Object[] { "not a builder", null }) { + String desc = name + " on " + instance; + Throwable expected = assertThrows(Throwable.class, + () -> _findProperty(vanilla, name).setAndReturn(instance, c[1]), desc); + Throwable actual = assertThrows(Throwable.class, + () -> _findProperty(MAPPER, name).setAndReturn(instance, c[1]), desc); + _assertSameFailure(desc + ", " + expected, expected, actual); + } + } + for (String name : new String[] { "str", "list" }) { + SettableBeanProperty prop = _findProperty(MAPPER, SkipNullsBuilt.class, name); + assertTrue(prop instanceof OptimizedSettableBeanProperty, name); + SkipNullsBuilder builder = new SkipNullsBuilder(); + assertSame(builder, prop.setAndReturn(builder, null)); + assertEquals("default", builder.str); + assertEquals(Arrays.asList("keep"), builder.list); + } + } + @Test public void testAccessModifierOverridesMatchVanilla() throws Exception { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java index 8924d2cb5..d23a07d27 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -7,7 +7,10 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -123,6 +126,38 @@ public void testShadowedType() throws Exception assertEquals(MethodProperty.class, _findProperty(bean, "level").getClass()); } + // Nor may the check load the return type, which the generated setter never does: + // the loader would define its own copy, and the code using it might then fail + @Test + public void testReturnTypeIsNotLoaded() throws Exception + { + ChildFirstLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Chain.class, ChainBuilder.class, ChainBuilt.class)); + Class built = Class.forName(ChainBuilt.class.getName(), true, loader); + + ChainBuilderBase.setterCalls = 0; + Object result = MAPPER.readValue(a2q("{'level':'HIGH'}"), built); + assertEquals("HIGH", built.getField("level").get(result)); + assertEquals(1, ChainBuilderBase.setterCalls); + assertFalse(loader.defined.contains(Chain.class.getName())); + } + + // A loader that fails (other than not finding the class) only stops the optimization + @Test + public void testFailingLoader() throws Exception + { + ChildFirstLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(LevelBuilder.class, LevelBuilt.class), + Collections.singleton(Level.class.getName())); + Class built = Class.forName(LevelBuilt.class.getName(), true, loader); + + LevelBuilderBase.setterCalls = 0; + Object result = MAPPER.readValue(a2q("{'level':'HIGH'}"), built); + assertEquals("HIGH", built.getField("level").get(result)); + assertEquals(1, LevelBuilderBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(built, "level").getClass()); + } + // Same for the setter's return type: nothing casts to it, but calling the setter // names it, and the JVM requires both loaders to agree on it @Test @@ -226,18 +261,29 @@ private static SettableBeanProperty _findProperty(ObjectMapper mapper, Class return deser.findProperty(name); } - // Defines its own copies of the given classes, delegating everything else + // Defines its own copies of the given classes, delegating everything else, and + // fails for the names it's told to static final class ChildFirstLoader extends ClassLoader { private final List> _copied; + private final Set _failing; + final Set defined = Collections.synchronizedSet(new HashSet<>()); ChildFirstLoader(ClassLoader parent, List> copied) { + this(parent, copied, Collections.emptySet()); + } + + ChildFirstLoader(ClassLoader parent, List> copied, Set failing) { super(parent); _copied = copied; + _failing = failing; } @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (_failing.contains(name)) { + throw new IllegalStateException("loader closed: " + name); + } synchronized (getClassLoadingLock(name)) { Class c = findLoadedClass(name); if (c == null) { @@ -245,6 +291,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE if (copied.getName().equals(name)) { byte[] bytes = _bytesOf(copied); c = defineClass(name, bytes, 0, bytes.length); + defined.add(name); } } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBuilderBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBuilderBase.java new file mode 100644 index 000000000..cb61009c5 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBuilderBase.java @@ -0,0 +1,16 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +// The package-private setter is invisible to subclasses in other packages, but +// Jackson finds it through the annotation and calls it through reflection +public class HiddenSetterBuilderBase +{ + protected int y; + + @JsonProperty("y") + HiddenSetterBuilderBase withY(int v) { + y = v; + return this; + } +} From 3fc809683d41eb571e183a12ce2616423886d8f3 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 08:29:57 -0400 Subject: [PATCH 15/34] Only use generated builder setters where Jackson would make the setter accessible --- .../deser/BBDeserializerModifier.java | 12 ++- .../deser/OptimizedSettableBeanProperty.java | 7 +- .../module/blackbird/deser/TestBuilders.java | 91 +++++++++++++++---- 3 files changed, 84 insertions(+), 26 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 8d0f9ad24..e3596532b 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -186,12 +186,14 @@ private void nextProperty(SettableBeanProperty prop, if (!ReflectionHack.canResolveClass(lookup, method.getDeclaringClass())) { return; } - // Builder setters used to go through reflection, which without - // CAN_OVERRIDE_ACCESS_MODIFIERS can only call public setters of public - // classes; the generated one must not call any others + // Builder setters used to go through reflection, which applies Java's + // access rules (modules included) unless Jackson made the setter accessible, + // as `ClassUtil.checkAndFixAccess` does only in this case; the generated + // setter ignores those rules, so it may only call what reflection could + boolean isPublic = Modifier.isPublic(method.getModifiers()) + && Modifier.isPublic(method.getDeclaringClass().getModifiers()); optimizeBuilderSetter = config.canOverrideAccessModifiers() - || (Modifier.isPublic(method.getModifiers()) - && Modifier.isPublic(method.getDeclaringClass().getModifiers())); + && (!isPublic || config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); } else { return; //setter = lookup.unreflectGetter((Field) jdkMember); diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 229d02dd1..39d204ce5 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -191,9 +191,12 @@ private boolean _isAccessorAccessFailure(Object accessor, Object bean, Throwable StackTraceElement[] trace; try { trace = e.getStackTrace(); - } catch (RuntimeException traceFailure) { + } catch (Throwable traceFailure) { // a subclass can override it; that is no evidence either, and mustn't - // replace the setter's failure + // replace the setter's failure (unless the JVM itself is failing) + if (traceFailure instanceof VirtualMachineError) { + throw (VirtualMachineError) traceFailure; + } return false; } // No trace (stack traces disabled, or a stackless throwable) is no evidence; diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index b2a3aab75..1abc1f279 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -453,6 +453,36 @@ public ProtectedSetterBuilt build() { } } + // Public, so Jackson only makes it accessible with OVERRIDE_PUBLIC_ACCESS_MODIFIERS; + // otherwise reflection applies Java's access rules (and modules), as must Blackbird + public static final class PublicSetterBuilder + { + int value; + int reflectiveCalls; + + public PublicSetterBuilder withValue(int v) { + reflectiveCalls += reflectiveCall(); + value = v; + return this; + } + + public PublicSetterBuilt build() { + return new PublicSetterBuilt(value, reflectiveCalls); + } + } + + @JsonDeserialize(builder=PublicSetterBuilder.class) + public static final class PublicSetterBuilt + { + public final int value; + public final int reflectiveCalls; + + PublicSetterBuilt(int value, int reflectiveCalls) { + this.value = value; + this.reflectiveCalls = reflectiveCalls; + } + } + @JsonDeserialize(builder=ProtectedSetterBuilder.class) public static final class ProtectedSetterBuilt { @@ -489,6 +519,8 @@ public ChildBuilt build() { static class ChildBuilt { } // Overrides `getStackTrace`, which `MethodProperty` never calls + static Throwable traceFailure; + @SuppressWarnings("serial") static class TraceFailingException extends SecurityException { @@ -498,7 +530,10 @@ static class TraceFailingException extends SecurityException @Override public StackTraceElement[] getStackTrace() { - throw new IllegalStateException("no trace for you"); + if (traceFailure instanceof Error) { + throw (Error) traceFailure; + } + throw (RuntimeException) traceFailure; } } @@ -945,20 +980,35 @@ public void testAccessModifierOverridesMatchVanilla() throws Exception { String json = a2q("{'value':42}"); for (boolean canOverride : new boolean[] { true, false }) { - ObjectMapper vanilla = JsonMapper.builder() - .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride).build(); - ObjectMapper blackbird = mapperBuilder() - .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride).build(); - if (canOverride) { - assertEquals(42, vanilla.readValue(json, ProtectedSetterBuilt.class).value); - assertEquals(42, blackbird.readValue(json, ProtectedSetterBuilt.class).value); - continue; + for (boolean overridePublic : new boolean[] { true, false }) { + String desc = "canOverride=" + canOverride + ", overridePublic=" + overridePublic; + ObjectMapper vanilla = JsonMapper.builder() + .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride) + .configure(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS, overridePublic).build(); + ObjectMapper blackbird = mapperBuilder() + .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride) + .configure(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS, overridePublic).build(); + + // reflection can only call the protected setter if Jackson made it accessible + if (canOverride) { + assertEquals(42, vanilla.readValue(json, ProtectedSetterBuilt.class).value, desc); + assertEquals(42, blackbird.readValue(json, ProtectedSetterBuilt.class).value, desc); + } else { + protectedCalls = 0; + Throwable expected = assertThrows(Throwable.class, + () -> vanilla.readValue(json, ProtectedSetterBuilt.class), desc); + Throwable actual = assertThrows(Throwable.class, + () -> blackbird.readValue(json, ProtectedSetterBuilt.class), desc); + _assertSameFailure(desc + ", " + expected, expected, actual); + assertEquals(0, protectedCalls, desc); + } + + // the public one always works here, but only goes through the generated + // setter if Jackson would have made it accessible + PublicSetterBuilt result = blackbird.readValue(json, PublicSetterBuilt.class); + assertEquals(42, result.value, desc); + assertEquals((canOverride && overridePublic) ? 0 : 1, result.reflectiveCalls, desc); } - protectedCalls = 0; - Throwable expected = assertThrows(Throwable.class, () -> vanilla.readValue(json, ProtectedSetterBuilt.class)); - Throwable actual = assertThrows(Throwable.class, () -> blackbird.readValue(json, ProtectedSetterBuilt.class)); - _assertSameFailure(String.valueOf(expected), expected, actual); - assertEquals(0, protectedCalls); } } @@ -976,11 +1026,14 @@ public void testValueOfAnotherTypeMatchesVanilla() throws Exception public void testFailingStackTraceKeepsSetterFailure() throws Exception { String json = a2q("{'x':1}"); - Throwable expected = assertThrows(Throwable.class, - () -> newVanillaJSONMapper().readValue(json, TraceFailingBuilt.class)); - Throwable actual = assertThrows(Throwable.class, () -> MAPPER.readValue(json, TraceFailingBuilt.class)); - assertEquals(TraceFailingException.class, actual.getCause().getClass()); - _assertSameFailure(String.valueOf(expected), expected, actual); + for (Throwable t : new Throwable[] { new IllegalStateException("no trace"), new AssertionError("no trace") }) { + traceFailure = t; + Throwable expected = assertThrows(Throwable.class, + () -> newVanillaJSONMapper().readValue(json, TraceFailingBuilt.class)); + Throwable actual = assertThrows(Throwable.class, () -> MAPPER.readValue(json, TraceFailingBuilt.class)); + assertEquals(TraceFailingException.class, actual.getCause().getClass(), String.valueOf(t)); + _assertSameFailure(t + ", " + expected, expected, actual); + } } // `TextNode(null)` reads as a VALUE_STRING whose text is null From 88570406a967070543835fabfcbfc108c6577e48 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 08:51:49 -0400 Subject: [PATCH 16/34] Match MethodProperty for bean setter errors, skipped nulls and null text --- .../deser/OptimizedSettableBeanProperty.java | 9 +- .../deser/SettableBooleanProperty.java | 4 +- .../blackbird/deser/SettableIntProperty.java | 4 +- .../blackbird/deser/SettableLongProperty.java | 4 +- .../deser/SettableObjectProperty.java | 13 +- .../deser/SettableStringProperty.java | 18 +- .../blackbird/deser/TestSetterFailures.java | 188 ++++++++++++++++++ 7 files changed, 228 insertions(+), 12 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 1760dadc9..4f34113d7 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -114,7 +114,8 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, * * @since 2.9 */ - protected void _reportProblem(Object accessor, Object thunk, Object bean, Object value, Throwable e) + protected void _reportProblem(JsonParser p, Object accessor, Object thunk, Object bean, Object value, + Throwable e) throws IOException { if (_isAccessorAccessFailure(accessor, thunk, bean, e)) { @@ -122,6 +123,12 @@ protected void _reportProblem(Object accessor, Object thunk, Object bean, Object delegate.set(bean, value); return; } + // These used to always be retried through reflection, which reported the + // setter's failure the way `MethodProperty` does (`p` is only for the location); + // keep that, rather than letting an `Error` escape unwrapped + if ((e instanceof IllegalAccessError) || (e instanceof SecurityException)) { + _throwAsIOE(p, new InvocationTargetException(e), value); + } if (e instanceof IOException) { throw (IOException) e; } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 1902d3842..5a23d34b8 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -48,7 +48,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, b, e); + _reportProblem(p, _optimizedSetter, _optimizedSetterAndReturn, bean, b, e); } } @@ -75,7 +75,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, b, e); + _reportProblem(null, _optimizedSetter, _optimizedSetterAndReturn, bean, b, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 85edf8f67..cebe95aa8 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); + _reportProblem(p, _optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } @@ -69,7 +69,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); + _reportProblem(null, _optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 117b94526..3b3938423 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); + _reportProblem(p, _optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } @@ -68,7 +68,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); + _reportProblem(null, _optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 70ca71169..8a7d1ba14 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -61,7 +61,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } - set(bean, value); + _set(p, bean, value); } @Override @@ -99,10 +99,19 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object v) throws IOException { + // as in `MethodProperty`, only here: `deserializeAndSet` already skipped the + // nulls it should + if ((v == null) && _skipNulls) { + return; + } + _set(null, bean, v); + } + + private void _set(JsonParser p, Object bean, Object v) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, v, e); + _reportProblem(p, _optimizedSetter, _optimizedSetterAndReturn, bean, v, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 7fd4a9bfd..10b65b200 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -38,11 +38,14 @@ protected SettableBeanProperty withDelegate(SettableBeanProperty del) { @Override public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean) throws IOException { - if (!p.hasToken(JsonToken.VALUE_STRING)) { + String text = p.hasToken(JsonToken.VALUE_STRING) ? p.getText() : null; + // some parsers give null text (e.g. for a `TextNode(null)`), which needs the + // delegate's null handling + if (text == null) { delegate.deserializeAndSet(p, ctxt, bean); return; } - set(bean, p.getText()); + _set(p, bean, text); } @Override @@ -62,10 +65,19 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, @Override public void set(Object bean, Object value) throws IOException { + // as in `MethodProperty`, only here: `deserializeAndSet` already skipped the + // nulls it should + if ((value == null) && _skipNulls) { + return; + } + _set(null, bean, value); + } + + private void _set(JsonParser p, Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, (String) value); } catch (Throwable e) { - _reportProblem(_optimizedSetter, _optimizedSetterAndReturn, bean, value, e); + _reportProblem(p, _optimizedSetter, _optimizedSetterAndReturn, bean, value, e); } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java index be52a9008..aeb588910 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -1,9 +1,12 @@ package com.fasterxml.jackson.module.blackbird.deser; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; @@ -14,15 +17,25 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; import com.fasterxml.jackson.module.blackbird.deser.access.InaccessibleSetterValue; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public class TestSetterFailures extends BlackbirdTestBase { @@ -46,6 +59,81 @@ static class FluentBean { public FluentBean setB(boolean value) { failOnce(); return this; } } + // Always fail, so reporting can be compared with plain Jackson + private static Throwable alwaysThrow; + + static class AlwaysFailingBean { + public void setStr(String value) { alwaysFail(); } + public void setI(int value) { alwaysFail(); } + } + + private static void alwaysFail() { + calls++; + if (alwaysThrow instanceof Error) { + throw (Error) alwaysThrow; + } + throw (RuntimeException) alwaysThrow; + } + + // `set()` skips nulls as `MethodProperty.set` does; properties before the creator's + // are buffered and assigned through it + static class SkipNullsBean { + final int id; + String str = "default"; + List list = Arrays.asList("keep"); + + @JsonCreator + SkipNullsBean(@JsonProperty("id") int id) { this.id = id; } + + @JsonSetter(nulls=Nulls.SKIP) + public void setStr(String value) { calls++; str = value; } + + @JsonSetter(nulls=Nulls.SKIP) + public void setList(List value) { calls++; list = value; } + } + + static class NullTextBean { + String skipped = "default"; + String plain = "default"; + + @JsonSetter(nulls=Nulls.SKIP) + public void setSkipped(String value) { skipped = value; } + + public void setPlain(String value) { plain = value; } + } + + // The bean is a hidden subclass, whose setters are left out of stack traces as + // the generated accessors are + private static Class hiddenVoid, hiddenFluent; + + public static class HideableVoidBean { + @JsonCreator + public static HideableVoidBean create() throws Exception { + return (HideableVoidBean) hiddenVoid.getConstructor().newInstance(); + } + public void setStr(String value) { } + } + + public static class HiddenVoidBean extends HideableVoidBean { + public HiddenVoidBean() { } + @Override + public void setStr(String value) { calls++; throw new SecurityException("thrown by hidden setter"); } + } + + public static class HideableFluentBean { + @JsonCreator + public static HideableFluentBean create() throws Exception { + return (HideableFluentBean) hiddenFluent.getConstructor().newInstance(); + } + public HideableFluentBean setStr(String value) { return this; } + } + + public static class HiddenFluentBean extends HideableFluentBean { + public HiddenFluentBean() { } + @Override + public HideableFluentBean setStr(String value) { calls++; throw new SecurityException("thrown by hidden setter"); } + } + // A retry would succeed and hide the original failure. private static void failOnce() { if (calls++ == 0) { @@ -112,6 +200,106 @@ public void testSetDoesNotRetrySetterFailures() throws Exception { } } + // Not retried, these are still reported as `MethodProperty` reports them (as they + // were after the retry), not as the setter's raw `Error` or exception + @Test + public void testAccessErrorsAreReportedAsVanilla() throws Exception { + for (boolean wrap : new boolean[] { true, false }) { + ObjectMapper vanilla = JsonMapper.builder().configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + ObjectMapper blackbird = mapperBuilder().configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + for (Throwable t : new Throwable[] { new SecurityException("se"), new IllegalAccessError("iae") }) { + for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'i':42}") }) { + String desc = "wrap=" + wrap + ", " + t + ", " + json; + alwaysThrow = t; + calls = 0; + Throwable expected = assertThrows(Throwable.class, () -> vanilla.readValue(json, AlwaysFailingBean.class)); + Throwable actual = assertThrows(Throwable.class, () -> blackbird.readValue(json, AlwaysFailingBean.class)); + assertEquals(2, calls, desc); + assertEquals(expected.getClass(), actual.getClass(), desc); + JsonMappingException e = (JsonMappingException) expected; + JsonMappingException a = (JsonMappingException) actual; + assertEquals(e.getOriginalMessage(), a.getOriginalMessage(), desc); + assertEquals(e.getPathReference(), a.getPathReference(), desc); + assertEquals(e.getLocation(), a.getLocation(), desc); + assertSame(t, a.getCause(), desc); + } + } + } + } + + @Test + public void testSetSkipsNulls() throws Exception { + for (String name : new String[] { "str", "list" }) { + SettableBeanProperty property = property(newObjectMapper(), SkipNullsBean.class, name); + assertTrue(property instanceof OptimizedSettableBeanProperty, name); + SkipNullsBean bean = new SkipNullsBean(1); + calls = 0; + property.set(bean, null); + assertEquals(0, calls, name); + } + // with the nulls before the creator property, so they're buffered + String json = a2q("{'str':null,'list':null,'id':1}"); + for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), newObjectMapper() }) { + calls = 0; + SkipNullsBean bean = mapper.readValue(json, SkipNullsBean.class); + assertEquals("default", bean.str); + assertEquals(Arrays.asList("keep"), bean.list); + assertEquals(0, calls); + } + } + + // `TextNode(null)` reads as a VALUE_STRING whose text is null + @Test + public void testNullTextMatchesVanilla() throws Exception { + assertTrue(property(newObjectMapper(), NullTextBean.class, "skipped") instanceof OptimizedSettableBeanProperty); + ObjectNode tree = JsonNodeFactory.instance.objectNode(); + tree.set("skipped", new TextNode(null)); + tree.set("plain", new TextNode(null)); + for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), newObjectMapper() }) { + NullTextBean bean = mapper.treeToValue(tree, NullTextBean.class); + assertEquals("default", bean.skipped); + assertNull(bean.plain); + } + } + + @Test + public void testHiddenBeanSetterFailureIsNotRetried() throws Exception { + Method define; + Object noOptions; + try { + Class option = Class.forName("java.lang.invoke.MethodHandles$Lookup$ClassOption"); + noOptions = Array.newInstance(option, 0); + define = MethodHandles.Lookup.class.getMethod("defineHiddenClass", + byte[].class, boolean.class, noOptions.getClass()); + } catch (ReflectiveOperationException e) { + assumeTrue(false, "hidden classes need Java 15+"); + return; + } + hiddenVoid = defineHidden(define, noOptions, HiddenVoidBean.class); + hiddenFluent = defineHidden(define, noOptions, HiddenFluentBean.class); + for (Class type : new Class[] { HideableVoidBean.class, HideableFluentBean.class }) { + assertTrue(property(newObjectMapper(), type, "str") instanceof OptimizedSettableBeanProperty); + for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), newObjectMapper() }) { + calls = 0; + assertThrows(JsonMappingException.class, () -> mapper.readValue(a2q("{'str':'foo'}"), type)); + assertEquals(1, calls, type.getSimpleName()); + } + } + } + + private static Class defineHidden(Method define, Object noOptions, Class type) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (InputStream in = type.getResourceAsStream("/" + type.getName().replace('.', '/') + ".class")) { + byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + } + } + return ((MethodHandles.Lookup) define.invoke(MethodHandles.lookup(), out.toByteArray(), true, noOptions)) + .lookupClass(); + } + private void assertRunsOnce(Executable action, String description) { calls = 0; Throwable thrown = assertThrows(Throwable.class, action, description); From 6530a021c1554e4515a2b9e394bedf87a9531eda Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 09:00:17 -0400 Subject: [PATCH 17/34] Skip generated setters the accessor can't call --- .../deser/BBDeserializerModifier.java | 27 +- .../module/blackbird/util/ReflectionHack.java | 129 ++++++++++ .../deser/TestInaccessibleSetterTypes.java | 237 ++++++++++++++++++ .../otherpackage/HiddenSetterBeanBase.java | 17 ++ .../blackbird/deser/otherpackage/Mode.java | 4 + .../deser/otherpackage/ModeBeanBase.java | 28 +++ .../otherpackage/ProtectedSetterBeanBase.java | 17 ++ .../blackbird/deser/shadowed/Chain.java | 3 + .../blackbird/deser/shadowed/ChainBean.java | 3 + .../deser/shadowed/ChainBeanBase.java | 16 ++ .../blackbird/deser/shadowed/Level.java | 3 + .../blackbird/deser/shadowed/LevelBean.java | 3 + .../deser/shadowed/LevelBeanBase.java | 15 ++ .../blackbird/deser/shadowed/OwnerBean.java | 17 ++ .../deser/shadowed/owner/OwnerBase.java | 17 ++ .../deser/shadowed/owner/OwnerBridge.java | 3 + .../blackbird/util/TestReflectionHack.java | 45 ++++ 17 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ProtectedSetterBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBean.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBean.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index a0013b45b..c267c0550 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -172,12 +172,37 @@ private void nextProperty(SettableBeanProperty prop, Class type; if (jdkMember instanceof Method && prop instanceof MethodProperty) { final Method method = (Method) jdkMember; - setter = lookup.unreflect(method); + try { + setter = lookup.unreflect(method); + } catch (IllegalAccessException e) { + // e.g. an inherited protected setter, where the lookup class isn't the + // subclass (as with `CrossLoaderAccess` on some JDKs); reflection can still call it + return; + } type = ((AnnotatedMethod) member).getRawParameterType(0); + // For an inherited protected setter the handle's receiver is narrowed to + // the lookup class, but calling it still names the declaring class + if (!ReflectionHack.canResolveClass(lookup, method.getDeclaringClass())) { + return; + } } else { return; //setter = lookup.unreflectGetter((Field) jdkMember); } + // The generated accessor casts to each type in the setter's signature, so + // one it can't access (like a package-private type from another package) + // would fail on every call; leave those to reflection + for (Class paramType : setter.type().parameterArray()) { + if (!ReflectionHack.canAccessClass(lookup, paramType)) { + return; + } + } + // Calling the setter also names its return type, which nothing casts to, but + // the JVM still requires to be the same class from the accessor's loader. + // The accessor never loads it, so neither may this check. + if (!ReflectionHack.isSameClassWithoutLoading(lookup, setter.type().returnType())) { + return; + } if (type.isPrimitive()) { if (type == Integer.TYPE) { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java index eb0f9e978..3f18f10ff 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java @@ -4,6 +4,7 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; import java.util.logging.Level; import java.util.logging.Logger; @@ -27,11 +28,117 @@ public static Lookup privateLookupIn(Class lookup, MethodHandles.Lookup orig) return Java8.privateLookupIn(lookup); } + /** + * Whether code defined with {@code lookup}, such as a lambda it spins, can use + * {@code type} where it must also access it, as in a cast. If not, that code + * fails when it runs, not when it is created. + */ + public static boolean canAccessClass(Lookup lookup, Class type) { + type = elementType(type); + if (type.isPrimitive()) { + return true; + } + try { + if (!resolvesTo(lookup, type)) { + return false; + } + if (Java9Up.ACCESS_CLASS != null) { + return Java9Up.canAccessClass(lookup, type); + } + return canAccessClassWithoutModules(lookup.lookupClass(), type); + } catch (SecurityException e) { + // e.g. a security manager denying `getClassLoader` + return false; + } + } + + /** + * Like {@link #canAccessClass}, for a type that code refers to and so resolves + * (as the owner of a method it calls), but need not be able to access: it must + * resolve to the same class. + */ + public static boolean canResolveClass(Lookup lookup, Class type) { + type = elementType(type); + if (type.isPrimitive()) { + return true; + } + try { + return resolvesTo(lookup, type); + } catch (SecurityException e) { + return false; + } + } + + /** + * Whether {@code type}, which code defined with {@code lookup} only names in a + * descriptor (as a called method's return type) and so never loads itself, is + * known to be the same class from the lookup class's loader. Unlike + * {@link #canResolveClass} this doesn't ask that loader, which could make it + * define its own copy and break code that relies on it never doing so. + */ + public static boolean isSameClassWithoutLoading(Lookup lookup, Class type) { + type = elementType(type); + if (type.isPrimitive() || type.getName().startsWith("java.")) { + return true; + } + try { + return type.getClassLoader() == lookup.lookupClass().getClassLoader(); + } catch (SecurityException e) { + return false; + } + } + + private static Class elementType(Class type) { + while (type.isArray()) { + type = type.getComponentType(); + } + return type; + } + + // That code refers to `type` by name, resolved by the lookup class's loader, + // which may find a different class of the same name (e.g. a child-first loader + // with its own copy) or none at all + private static boolean resolvesTo(Lookup lookup, Class type) { + ClassLoader loader = lookup.lookupClass().getClassLoader(); + // a class its loader defined is what that loader resolves its name to, + // without asking it to load anything + if (type.getClassLoader() == loader) { + return true; + } + try { + return Class.forName(type.getName(), false, loader) == type; + } catch (ClassNotFoundException | LinkageError | RuntimeException e) { + // including whatever else a misbehaving or closed loader throws + return false; + } + } + + // Without modules, public classes are accessible from anywhere, others only + // from the same runtime package (which includes the bean's own class, often + // package-private). `getModifiers` reports the declared modifiers of nested + // classes, so a protected one is (safely) treated as inaccessible from other + // packages, though the JVM would allow it. + static boolean canAccessClassWithoutModules(Class lookupClass, Class type) { + if (Modifier.isPublic(type.getModifiers())) { + return true; + } + return (type.getClassLoader() == lookupClass.getClassLoader()) + && packageName(type).equals(packageName(lookupClass)); + } + + private static String packageName(Class type) { + String name = type.getName(); + int dot = name.lastIndexOf('.'); + return (dot < 0) ? "" : name.substring(0, dot); + } + static class Java9Up { static final MethodHandle FACTORY; + static final MethodHandle ACCESS_CLASS; static { FACTORY = init(); + ACCESS_CLASS = (FACTORY == null) ? null : initAccessClass(); } private static MethodHandle init() { @@ -51,6 +158,17 @@ private static MethodHandle init() { } } + private static MethodHandle initAccessClass() { + try { + return MethodHandles.lookup().findVirtual( + MethodHandles.Lookup.class, + "accessClass", + MethodType.methodType(Class.class, Class.class)); + } catch (ReflectiveOperationException e) { + return null; + } + } + public static Lookup privateLookupIn(Class lookup, Lookup orig) { try { return (Lookup) FACTORY.invokeExact(lookup, orig); @@ -60,6 +178,17 @@ public static Lookup privateLookupIn(Class lookup, Lookup orig) { throw Sneaky.throwAnyway(t); } } + + public static boolean canAccessClass(Lookup lookup, Class type) { + try { + Class unused = (Class) ACCESS_CLASS.invokeExact(lookup, type); + return true; + } catch (IllegalAccessException e) { + return false; + } catch (Throwable t) { + throw Sneaky.throwAnyway(t); + } + } } static class Java8 { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java new file mode 100644 index 000000000..78a243e64 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -0,0 +1,237 @@ +package com.fasterxml.jackson.module.blackbird.deser; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; +import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.databind.deser.impl.MethodProperty; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.module.blackbird.BlackbirdModule; +import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.HiddenSetterBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ModeBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ProtectedSetterBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.Chain; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.ChainBean; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.ChainBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.Level; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBean; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.LevelBeanBase; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.OwnerBean; +import com.fasterxml.jackson.module.blackbird.deser.shadowed.owner.OwnerBase; + +import static org.junit.jupiter.api.Assertions.*; + +// Setters whose generated accessor would fail when called, even though reflection +// can call them, must be left to reflection +public class TestInaccessibleSetterTypes extends BlackbirdTestBase +{ + // `setMode` and `setModes` take a type that is package-private in another + // package, which reflection can pass but a generated accessor can't refer to + static final class ModeBean extends ModeBeanBase { } + + static final class ProtectedBean extends ProtectedSetterBeanBase { } + + static final class HiddenSetterBean extends HiddenSetterBeanBase { } + + private final ObjectMapper MAPPER = newObjectMapper(); + + @Test + public void testInaccessibleParameterType() throws Exception + { + ModeBeanBase.setterCalls = 0; + ModeBean result = MAPPER.readValue(a2q("{'mode':'FAST','modes':['SAFE'],'name':'foo'}"), ModeBean.class); + assertEquals("FAST", result.mode()); + assertEquals("[SAFE]", result.modes()); + assertEquals("foo", result.name()); + assertEquals(2, ModeBeanBase.setterCalls); + // rather than failing (and falling back) on every call + assertEquals(MethodProperty.class, _findProperty(ModeBean.class, "mode").getClass()); + assertEquals(MethodProperty.class, _findProperty(ModeBean.class, "modes").getClass()); + SettableBeanProperty name = _findProperty(ModeBean.class, "name"); + assertTrue(name instanceof OptimizedSettableBeanProperty, name.getClass().getName()); + } + + // `unreflect` refuses some inherited setters that reflection can call; these used + // to fail the whole deserializer + @Test + public void testInheritedSettersFromAnotherPackage() throws Exception + { + assertEquals("foo", MAPPER.readValue(a2q("{'str':'foo'}"), ProtectedBean.class).str()); + assertEquals(3, MAPPER.readValue(a2q("{'y':3}"), HiddenSetterBean.class).y()); + } + + // The bean's loader has its own copy of `Level`, so a generated accessor would + // resolve that one and fail to cast the inherited setter's `Level` to it + @Test + public void testShadowedParameterType() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Level.class, LevelBean.class)); + Class bean = Class.forName(LevelBean.class.getName(), true, loader); + assertNotSame(LevelBean.class, bean); + + LevelBeanBase.setterCalls = 0; + LevelBeanBase result = (LevelBeanBase) MAPPER.readValue(a2q("{'level':'HIGH'}"), bean); + assertEquals("HIGH", result.level()); + assertEquals(1, LevelBeanBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(bean, "level").getClass()); + } + + // Same for a fluent setter's return type: nothing casts to it, but calling the + // setter names it, and the JVM requires both loaders to agree on it + @Test + public void testShadowedReturnType() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Chain.class, ChainBean.class)); + Class bean = Class.forName(ChainBean.class.getName(), true, loader); + // the bean's loader has its own `Chain`, as it would once anything there used it + assertNotSame(Chain.class, Class.forName(Chain.class.getName(), false, loader)); + + ChainBeanBase.setterCalls = 0; + ChainBeanBase result = (ChainBeanBase) MAPPER.readValue(a2q("{'level':'HIGH'}"), bean); + assertEquals("HIGH", result.level()); + assertEquals(1, ChainBeanBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(bean, "level").getClass()); + } + + // Nor may the check load the return type, which the generated setter never does: + // the loader would define its own copy, and the code using it might then fail + @Test + public void testReturnTypeIsNotLoaded() throws Exception + { + ChildFirstLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Chain.class, ChainBean.class)); + Class bean = Class.forName(ChainBean.class.getName(), true, loader); + + ChainBeanBase.setterCalls = 0; + ChainBeanBase result = (ChainBeanBase) MAPPER.readValue(a2q("{'level':'HIGH'}"), bean); + assertEquals("HIGH", result.level()); + assertEquals(1, ChainBeanBase.setterCalls); + assertFalse(loader.defined.contains(Chain.class.getName())); + } + + // And for the declaring class of an inherited protected setter, which isn't in the + // handle's type (its receiver is narrowed to the bean), but still named by the call + @Test + public void testShadowedDeclaringClass() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(OwnerBase.class, OwnerBean.class)); + Class bean = Class.forName(OwnerBean.class.getName(), true, loader); + assertNotSame(OwnerBase.class, Class.forName(OwnerBase.class.getName(), false, loader)); + // a lookup in the bean itself, which the protected setter needs + MethodHandles.Lookup beanLookup = (MethodHandles.Lookup) bean.getMethod("lookup").invoke(null); + ObjectMapper mapper = JsonMapper.builder() + .addModule(new BlackbirdModule(c -> (c == bean) ? beanLookup : MethodHandles.lookup())) + .build(); + + OwnerBase.setterCalls = 0; + Object result = mapper.readValue(a2q("{'value':7}"), bean); + assertEquals(7, bean.getMethod("value").invoke(result)); + assertEquals(1, OwnerBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(mapper, bean, "value").getClass()); + } + + // A loader that fails (other than not finding the class) only stops the optimization + @Test + public void testFailingLoader() throws Exception + { + ClassLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(LevelBean.class), Collections.singleton(Level.class.getName())); + Class bean = Class.forName(LevelBean.class.getName(), true, loader); + + LevelBeanBase.setterCalls = 0; + LevelBeanBase result = (LevelBeanBase) MAPPER.readValue(a2q("{'level':'HIGH'}"), bean); + assertEquals("HIGH", result.level()); + assertEquals(1, LevelBeanBase.setterCalls); + assertEquals(MethodProperty.class, _findProperty(bean, "level").getClass()); + } + + private SettableBeanProperty _findProperty(Class type, String name) throws Exception + { + return _findProperty(MAPPER, type, name); + } + + private static SettableBeanProperty _findProperty(ObjectMapper mapper, Class type, String name) + throws Exception + { + DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) mapper.getDeserializationContext()) + .createInstance(mapper.getDeserializationConfig(), null, null); + BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( + mapper.constructType(type)); + return deser.findProperty(name); + } + + // Defines its own copies of the given classes, delegating everything else, and + // fails for the names it's told to + static final class ChildFirstLoader extends ClassLoader + { + private final List> _copied; + private final Set _failing; + final Set defined = Collections.synchronizedSet(new HashSet<>()); + + ChildFirstLoader(ClassLoader parent, List> copied) { + this(parent, copied, Collections.emptySet()); + } + + ChildFirstLoader(ClassLoader parent, List> copied, Set failing) { + super(parent); + _copied = copied; + _failing = failing; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (_failing.contains(name)) { + throw new IllegalStateException("loader closed: " + name); + } + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + for (Class copied : _copied) { + if (copied.getName().equals(name)) { + byte[] bytes = _bytesOf(copied); + c = defineClass(name, bytes, 0, bytes.length); + defined.add(name); + } + } + } + if (c == null) { + return super.loadClass(name, resolve); + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + private byte[] _bytesOf(Class type) throws ClassNotFoundException { + String resource = type.getName().replace('.', '/') + ".class"; + try (InputStream in = getParent().getResourceAsStream(resource)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + } + return out.toByteArray(); + } catch (Exception e) { + throw new ClassNotFoundException(type.getName(), e); + } + } + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBeanBase.java new file mode 100644 index 000000000..5cc80a39e --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/HiddenSetterBeanBase.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +// The package-private setter is invisible to subclasses in other packages, but +// Jackson finds it through the annotation and calls it through reflection +public class HiddenSetterBeanBase +{ + protected int y; + + @JsonProperty("y") + void setY(int v) { + y = v; + } + + public int y() { return y; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java new file mode 100644 index 000000000..1a8c0d17d --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/Mode.java @@ -0,0 +1,4 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +// package-private, so subclasses in other packages can't refer to it +enum Mode { FAST, SAFE } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java new file mode 100644 index 000000000..7d30ed4d7 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ModeBeanBase.java @@ -0,0 +1,28 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +public class ModeBeanBase +{ + public static int setterCalls; + + protected String mode; + protected String modes; + protected String name; + + public void setMode(Mode m) { + setterCalls++; + mode = m.name(); + } + + public void setModes(Mode[] m) { + setterCalls++; + modes = java.util.Arrays.toString(m); + } + + public void setName(String n) { + name = n; + } + + public String mode() { return mode; } + public String modes() { return modes; } + public String name() { return name; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ProtectedSetterBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ProtectedSetterBeanBase.java new file mode 100644 index 000000000..36eb10ec7 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/otherpackage/ProtectedSetterBeanBase.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.module.blackbird.deser.otherpackage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +// A subclass in another package can call the protected setter, but only through a +// lookup in that subclass, which Blackbird doesn't always have +public class ProtectedSetterBeanBase +{ + protected String str; + + @JsonProperty("str") + protected void setStr(String v) { + str = v; + } + + public String str() { return str; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java new file mode 100644 index 000000000..1b8180dea --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Chain.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public interface Chain { } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBean.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBean.java new file mode 100644 index 000000000..4c4153be2 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBean.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class ChainBean extends ChainBeanBase { } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBeanBase.java new file mode 100644 index 000000000..dd9484a54 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/ChainBeanBase.java @@ -0,0 +1,16 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class ChainBeanBase implements Chain +{ + public static int setterCalls; + + protected String level; + + public Chain setLevel(String l) { + setterCalls++; + level = l; + return this; + } + + public String level() { return level; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java new file mode 100644 index 000000000..cf775a818 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/Level.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public enum Level { LOW, HIGH } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java new file mode 100644 index 000000000..a87ae2359 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBean.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class LevelBean extends LevelBeanBase { } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java new file mode 100644 index 000000000..8e043b0f9 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/LevelBeanBase.java @@ -0,0 +1,15 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +public class LevelBeanBase +{ + public static int setterCalls; + + protected String level; + + public void setLevel(Level l) { + setterCalls++; + level = l.name(); + } + + public String level() { return level; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBean.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBean.java new file mode 100644 index 000000000..9df91e273 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/OwnerBean.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed; + +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.module.blackbird.deser.shadowed.owner.OwnerBridge; + +// in another package than the protected setter's declaring class, so its handle's +// receiver is narrowed to this class +public final class OwnerBean extends OwnerBridge +{ + // so Blackbird can be given a lookup in this class, as for protected setters it needs + public static MethodHandles.Lookup lookup() { + return MethodHandles.lookup(); + } + + public int value() { return value; } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java new file mode 100644 index 000000000..f2d8f13c0 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed.owner; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OwnerBase +{ + public static int setterCalls; + + protected int value; + + @JsonProperty("value") + protected Object withValue(int v) { + setterCalls++; + value = v; + return this; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java new file mode 100644 index 000000000..ec121e379 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBridge.java @@ -0,0 +1,3 @@ +package com.fasterxml.jackson.module.blackbird.deser.shadowed.owner; + +public class OwnerBridge extends OwnerBase { } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java new file mode 100644 index 000000000..ec08504ff --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/util/TestReflectionHack.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.module.blackbird.util; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ModeBeanBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestReflectionHack +{ + static class SamePackage { } + + private static final Lookup LOOKUP = MethodHandles.lookup(); + + @Test + public void testCanAccessClass() throws Exception + { + Class mode = _packagePrivateFromOtherPackage(); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, int.class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, int[].class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, String.class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, ModeBeanBase.class)); + assertTrue(ReflectionHack.canAccessClass(LOOKUP, SamePackage[].class)); + assertFalse(ReflectionHack.canAccessClass(LOOKUP, mode)); + assertFalse(ReflectionHack.canAccessClass(LOOKUP, java.lang.reflect.Array.newInstance(mode, 0).getClass())); + } + + // what Java 8 uses, which can't be picked on newer JDKs + @Test + public void testCanAccessClassWithoutModules() throws Exception + { + Class self = getClass(); + assertTrue(ReflectionHack.canAccessClassWithoutModules(self, String.class)); + assertTrue(ReflectionHack.canAccessClassWithoutModules(self, ModeBeanBase.class)); + assertTrue(ReflectionHack.canAccessClassWithoutModules(self, SamePackage.class)); + assertFalse(ReflectionHack.canAccessClassWithoutModules(self, _packagePrivateFromOtherPackage())); + } + + private static Class _packagePrivateFromOtherPackage() throws Exception { + return Class.forName("com.fasterxml.jackson.module.blackbird.deser.otherpackage.Mode"); + } +} From c352ebb93481e055193f9bc49a21a229c2500f6f Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 09:22:40 -0400 Subject: [PATCH 18/34] Don't load the declaring class, or check unsupported setter types, at setup --- .../deser/BBDeserializerModifier.java | 13 ++++++++--- .../module/blackbird/util/ReflectionHack.java | 19 +-------------- .../deser/TestInaccessibleSetterTypes.java | 23 +++++++++++++++++++ .../deser/shadowed/owner/OwnerBase.java | 8 +++++++ 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index c267c0550..4cdfd82a1 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -180,9 +180,16 @@ private void nextProperty(SettableBeanProperty prop, return; } type = ((AnnotatedMethod) member).getRawParameterType(0); - // For an inherited protected setter the handle's receiver is narrowed to - // the lookup class, but calling it still names the declaring class - if (!ReflectionHack.canResolveClass(lookup, method.getDeclaringClass())) { + // Only these get a generated setter, so don't check (or load) anything for others + if (type.isPrimitive() && (type != Integer.TYPE) && (type != Long.TYPE) && (type != Boolean.TYPE)) { + return; + } + // For an inherited protected setter the handle's receiver is narrowed to the + // lookup class, but on Java 8 calling it still names the declaring class (which + // is otherwise the receiver, checked below). Newer JDKs don't load it, so + // neither may this check. + if ((setter.type().parameterType(0) != method.getDeclaringClass()) + && !ReflectionHack.isSameClassWithoutLoading(lookup, method.getDeclaringClass())) { return; } } else { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java index 3f18f10ff..63b5635ff 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java @@ -52,28 +52,11 @@ public static boolean canAccessClass(Lookup lookup, Class type) { } } - /** - * Like {@link #canAccessClass}, for a type that code refers to and so resolves - * (as the owner of a method it calls), but need not be able to access: it must - * resolve to the same class. - */ - public static boolean canResolveClass(Lookup lookup, Class type) { - type = elementType(type); - if (type.isPrimitive()) { - return true; - } - try { - return resolvesTo(lookup, type); - } catch (SecurityException e) { - return false; - } - } - /** * Whether {@code type}, which code defined with {@code lookup} only names in a * descriptor (as a called method's return type) and so never loads itself, is * known to be the same class from the lookup class's loader. Unlike - * {@link #canResolveClass} this doesn't ask that loader, which could make it + * {@link #canAccessClass} this doesn't ask that loader, which could make it * define its own copy and break code that relies on it never doing so. */ public static boolean isSameClassWithoutLoading(Lookup lookup, Class type) { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java index 78a243e64..16ef6bba4 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -145,6 +145,29 @@ public void testShadowedDeclaringClass() throws Exception assertEquals(MethodProperty.class, _findProperty(mapper, bean, "value").getClass()); } + // Nor may checking the declaring class load it, when the generated setter wouldn't + // (or wouldn't exist at all, as for a `double` setter) + @Test + public void testDeclaringClassIsNotLoaded() throws Exception + { + for (boolean beanLookup : new boolean[] { false, true }) { + ChildFirstLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(OwnerBase.class, OwnerBean.class)); + Class bean = Class.forName(OwnerBean.class.getName(), true, loader); + ObjectMapper mapper = MAPPER; + if (beanLookup) { + MethodHandles.Lookup lookup = (MethodHandles.Lookup) bean.getMethod("lookup").invoke(null); + mapper = JsonMapper.builder() + .addModule(new BlackbirdModule(c -> (c == bean) ? lookup : MethodHandles.lookup())) + .build(); + } + Object result = mapper.readValue(a2q("{'value':7,'ratio':0.5}"), bean); + assertEquals(7, bean.getMethod("value").invoke(result)); + assertEquals(0.5, bean.getMethod("ratio").invoke(result)); + assertFalse(loader.defined.contains(OwnerBase.class.getName()), "beanLookup=" + beanLookup); + } + } + // A loader that fails (other than not finding the class) only stops the optimization @Test public void testFailingLoader() throws Exception diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java index f2d8f13c0..eb1fe4e78 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/shadowed/owner/OwnerBase.java @@ -7,6 +7,7 @@ public class OwnerBase public static int setterCalls; protected int value; + protected double ratio; @JsonProperty("value") protected Object withValue(int v) { @@ -14,4 +15,11 @@ protected Object withValue(int v) { value = v; return this; } + + // public, but of a type Blackbird doesn't generate setters for + public void setRatio(double r) { + ratio = r; + } + + public double ratio() { return ratio; } } From aca43ef072b367704839418de30554b75fb9ecc0 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 09:26:11 -0400 Subject: [PATCH 19/34] Don't optimize public JDK builder setters, which Jackson doesn't make accessible --- .../deser/BBDeserializerModifier.java | 25 ++++++++++---- .../module/blackbird/deser/TestBuilders.java | 33 +++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index e3596532b..ef3166c0c 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -187,13 +187,10 @@ private void nextProperty(SettableBeanProperty prop, return; } // Builder setters used to go through reflection, which applies Java's - // access rules (modules included) unless Jackson made the setter accessible, - // as `ClassUtil.checkAndFixAccess` does only in this case; the generated - // setter ignores those rules, so it may only call what reflection could - boolean isPublic = Modifier.isPublic(method.getModifiers()) - && Modifier.isPublic(method.getDeclaringClass().getModifiers()); - optimizeBuilderSetter = config.canOverrideAccessModifiers() - && (!isPublic || config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); + // access rules (modules included) unless Jackson made the setter accessible; + // the generated setter ignores those rules, so it may only call what + // reflection could + optimizeBuilderSetter = isMadeAccessible(config, method); } else { return; //setter = lookup.unreflectGetter((Field) jdkMember); @@ -249,6 +246,20 @@ private void nextProperty(SettableBeanProperty prop, } } + // Whether Jackson makes the setter accessible, as `ClassUtil.checkAndFixAccess` + // does for properties (and only if allowed to override access modifiers) + static boolean isMadeAccessible(DeserializationConfig config, Method setter) { + if (!config.canOverrideAccessModifiers()) { + return false; + } + Class declaringClass = setter.getDeclaringClass(); + boolean isPublic = Modifier.isPublic(setter.getModifiers()) + && Modifier.isPublic(declaringClass.getModifiers()); + return !isPublic + || (config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS) + && !ClassUtil.isJDKClass(declaringClass)); + } + // Returns null for void setters, which have nothing to return private T createThunk(Lookup lookup, Class thunkType, Class valueType, MethodHandle setter) throws Throwable, LambdaConversionException { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 1abc1f279..5535edf62 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.MapperFeature; @@ -33,6 +34,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.HiddenSetterBuilderBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ReplacingBuilderBase; @@ -975,6 +977,37 @@ public void testSetAndReturnWithUnusableBuilder() throws Exception } } + // The rule the generated builder setters follow is the one Jackson uses to make + // setters accessible, including leaving public JDK setters alone + @SuppressWarnings("deprecation") + @Test + public void testIsMadeAccessibleMatchesJackson() throws Exception + { + for (boolean canOverride : new boolean[] { true, false }) { + for (boolean overridePublic : new boolean[] { true, false }) { + DeserializationConfig config = mapperBuilder() + .configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, canOverride) + .configure(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS, overridePublic) + .build().getDeserializationConfig(); + // fresh `Method` copies, as each lookup returns + Method[] setters = { + javax.naming.CompositeName.class.getMethod("add", String.class), + PublicSetterBuilder.class.getMethod("withValue", int.class), + ProtectedSetterBuilder.class.getDeclaredMethod("withValue", int.class), + }; + for (Method setter : setters) { + // as `BeanDeserializerBuilder` and `MethodProperty.fixAccess` do + if (config.canOverrideAccessModifiers()) { + ClassUtil.checkAndFixAccess(setter, + config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); + } + assertEquals(setter.isAccessible(), BBDeserializerModifier.isMadeAccessible(config, setter), + "canOverride=" + canOverride + ", overridePublic=" + overridePublic + ", " + setter); + } + } + } + } + @Test public void testAccessModifierOverridesMatchVanilla() throws Exception { From 8d0e0149909102590fb5255905a8861819387b28 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 10:27:41 -0400 Subject: [PATCH 20/34] Cover polymorphic nulls for regular bean setters --- .../deser/SettableObjectProperty.java | 2 + .../blackbird/deser/TestSetterFailures.java | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 8a7d1ba14..e69d96c70 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -61,6 +61,8 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, } else { value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); } + // Do not call set(): MethodProperty passes nulls returned by polymorphic + // deserialization to the setter, even with Nulls.SKIP. _set(p, bean, value); } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java index aeb588910..0c749e745 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; @@ -92,6 +93,22 @@ static class SkipNullsBean { public void setList(List value) { calls++; list = value; } } + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, defaultImpl=Void.class) + static class Item { + public String name; + } + + static class PolymorphicNullBean { + int calls; + Item value = new Item(); + + @JsonSetter(nulls=Nulls.SKIP) + public void setVoid(Item v) { calls++; value = v; } + + @JsonSetter(nulls=Nulls.SKIP) + public PolymorphicNullBean setFluent(Item v) { calls++; value = v; return this; } + } + static class NullTextBean { String skipped = "default"; String plain = "default"; @@ -248,6 +265,33 @@ public void testSetSkipsNulls() throws Exception { } } + @Test + public void testPolymorphicNullMatchesVanilla() throws Exception { + ObjectMapper blackbird = newObjectMapper(); + for (String name : new String[] { "void", "fluent" }) { + SettableBeanProperty prop = property(blackbird, PolymorphicNullBean.class, name); + assertTrue(prop instanceof OptimizedSettableBeanProperty, name); + + PolymorphicNullBean direct = new PolymorphicNullBean(); + prop.set(direct, null); + assertEquals(0, direct.calls, name); + assertNotNull(direct.value, name); + + for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), blackbird }) { + PolymorphicNullBean explicit = mapper.readValue(a2q("{'" + name + "':null}"), PolymorphicNullBean.class); + assertEquals(0, explicit.calls, name); + assertNotNull(explicit.value, name); + + // MethodProperty passes a null from polymorphic deserialization to + // the setter, even with Nulls.SKIP. + PolymorphicNullBean polymorphic = mapper.readValue( + a2q("{'" + name + "':{'@type':'Unknown'}}"), PolymorphicNullBean.class); + assertEquals(1, polymorphic.calls, name); + assertNull(polymorphic.value, name); + } + } + } + // `TextNode(null)` reads as a VALUE_STRING whose text is null @Test public void testNullTextMatchesVanilla() throws Exception { From f0ec75fa41af6c5f732c7f9fcf543f42424a8506 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 10:39:21 -0400 Subject: [PATCH 21/34] Never retry builder setters after the generated setter fails --- .../deser/OptimizedSettableBeanProperty.java | 51 +++-------------- .../deser/SettableBooleanProperty.java | 2 +- .../blackbird/deser/SettableIntProperty.java | 2 +- .../blackbird/deser/SettableLongProperty.java | 2 +- .../deser/SettableObjectProperty.java | 2 +- .../deser/SettableStringProperty.java | 2 +- .../module/blackbird/deser/TestBuilders.java | 57 ++++++------------- .../deser/TestInaccessibleSetterTypes.java | 23 ++++---- 8 files changed, 38 insertions(+), 103 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 39d204ce5..39006a294 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -137,20 +137,17 @@ protected void _reportProblem(Object bean, Object value, Throwable e) } /** - * Like {@link #_reportProblem}, for {@link #setAndReturn}: an access failure of - * the generated accessor falls back to the delegate, and anything the setter - * throws is reported the way {@code MethodProperty} reports it. The parser is - * only used for the error location, and is null outside of + * Like {@link #_reportProblem}, for {@link #setAndReturn}: anything the generated + * setter throws is reported the way {@code MethodProperty} reports it, and never + * retried. Telling a failure of the generated accessor itself from one thrown by + * the setter would mean guessing from the stack trace, and a retry could run the + * setter twice; setup instead leaves the setters it can't call to reflection. + * The parser is only used for the error location, and is null outside of * {@link #deserializeSetAndReturn}, as it is for {@code MethodProperty}. */ - protected Object _reportProblemAndReturn(JsonParser p, Object accessor, Object bean, Object value, - Throwable e) + protected Object _reportProblemAndReturn(JsonParser p, Object value, Throwable e) throws IOException { - if (_isAccessorAccessFailure(accessor, bean, e)) { - _logAccessProblem(bean, e); - return _reflectiveSetAndReturn(p, bean, value); - } // `Method.invoke` wraps whatever the setter throws, and `MethodProperty` // translates that wrapper; doing the same keeps type, message, cause and path _throwAsIOE(p, new InvocationTargetException(e), value); @@ -179,40 +176,6 @@ protected Object _reflectiveSetAndReturn(JsonParser p, Object instance, Object v } } - // Retrying is only safe if the setter never ran, so require positive evidence: - // the JVM raises an access failure in the accessor's own frame, which is then - // on top of the stack (or, where generated frames are hidden from stack traces, - // this property's frame that called it). A throwable created anywhere else, - // such as one the setter saved and throws later, keeps its original failure. - private boolean _isAccessorAccessFailure(Object accessor, Object bean, Throwable e) { - if (!(e instanceof IllegalAccessError) && !(e instanceof SecurityException)) { - return false; - } - StackTraceElement[] trace; - try { - trace = e.getStackTrace(); - } catch (Throwable traceFailure) { - // a subclass can override it; that is no evidence either, and mustn't - // replace the setter's failure (unless the JVM itself is failing) - if (traceFailure instanceof VirtualMachineError) { - throw (VirtualMachineError) traceFailure; - } - return false; - } - // No trace (stack traces disabled, or a stackless throwable) is no evidence; - // setup already skips the accessors known to fail this way - if ((trace == null) || (trace.length == 0) || (trace[0] == null)) { - return false; - } - String top = trace[0].getClassName(); - if (top.equals(accessor.getClass().getName())) { - return true; - } - // A setter in a hidden class is left out of the trace too, which is only - // possible if the bean's own class is hidden (their names contain a '/') - return top.equals(getClass().getName()) && (bean.getClass().getName().indexOf('/') < 0); - } - private void _logAccessProblem(Object bean, Throwable e) { synchronized (this) { // yes, double-locking, so not guaranteed; but all we want here is to reduce likelihood diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index c80416c9d..d065d5680 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -98,7 +98,7 @@ private Object _setAndReturn(JsonParser p, Object instance, boolean b) throws IO Object result = _optimizedSetterAndReturn.apply(instance, b); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, b, e); + return _reportProblemAndReturn(p, b, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 49b232d61..5523be9d2 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -92,7 +92,7 @@ private Object _setAndReturn(JsonParser p, Object instance, int v) throws IOExce Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, v, e); + return _reportProblemAndReturn(p, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 754bd80d1..73a211d76 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -91,7 +91,7 @@ private Object _setAndReturn(JsonParser p, Object instance, long v) throws IOExc Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, v, e); + return _reportProblemAndReturn(p, v, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 0608eb283..41dc57cda 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -127,7 +127,7 @@ private Object _setAndReturn(JsonParser p, Object instance, Object value) throws Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, value, e); + return _reportProblemAndReturn(p, value, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 53a66f6c9..512d88b7d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -90,7 +90,7 @@ private Object _setAndReturn(JsonParser p, Object instance, Object value) throws Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, _optimizedSetterAndReturn, instance, value, e); + return _reportProblemAndReturn(p, value, e); } } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 5535edf62..c38827281 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -336,7 +336,7 @@ public FlakyBuilt build() { @JsonDeserialize(builder=FlakyBuilder.class) static class FlakyBuilt { } - // The builder is a hidden subclass whose setter fails after doing its work. Hidden + // The builder is a hidden subclass whose setter fails after doing its work. Its // frames are left out of stack traces, as the generated accessor's are. static Class hiddenBuilderClass; static int hiddenCalls; @@ -706,37 +706,6 @@ public void testNullReturningBuilder() throws Exception assertEquals(42, v.i); } - // An access failure is raised by the JVM in the generated accessor's own frame, - // before the setter runs; the anonymous classes stand in for that accessor. - // The reflective fallback must still return the new builder. - @Test - public void testAccessFailureFallsBackToReflection() throws Exception - { - ObjectMapper vanilla = newVanillaJSONMapper(); - DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) vanilla.getDeserializationContext()) - .createInstance(vanilla.getDeserializationConfig(), null, null); - BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( - vanilla.constructType(CopyingBuilt.class)); - SettableStringProperty prop = new SettableStringProperty(deser.findProperty("str"), - new BiConsumer() { - @Override - public void accept(Object bean, String v) { - throw new IllegalAccessError("simulated"); - } - }, - new BiFunction() { - @Override - public Object apply(Object bean, Object v) { - throw new IllegalAccessError("simulated"); - } - }, - CopyingBuilder.class); - - CopyingBuilder result = (CopyingBuilder) prop.setAndReturn(new CopyingBuilder(), "foo"); - assertEquals("foo", result.str); - assertEquals(1, result.reflectiveCalls); - } - // Whether the setter creates the throwable or saved it earlier, it may already // have done its work, so it must not run again. That includes throwables with no // stack trace, as with `-XX:-StackTraceInThrowable`. @@ -825,7 +794,7 @@ public void testThrowingSetterMatchesVanilla() throws Exception } // A setter in a hidden class looks like the generated accessor in a stack trace - // (both are left out), so its failure must not be mistaken for an accessor's + // (both are left out), but it must still run only once @Test public void testHiddenClassSetterFailureIsNotRetried() throws Exception { @@ -903,43 +872,49 @@ public void testUnusableBuilderInstanceMatchesVanilla() throws Exception } } - // If the generated setter fails to access it and the setter then fails through - // reflection, the error still has the JSON location + // A failure of the generated accessor itself (simulated by the anonymous classes) + // is reported like the setter's, without trying the setter again through + // reflection, and with the JSON location where there is one @Test - public void testAccessFailureFallbackKeepsLocation() throws Exception + public void testAccessorFailureIsNotRetried() throws Exception { ObjectMapper vanilla = newVanillaJSONMapper(); BeanDeserializerBase deser = (BeanDeserializerBase) ((DefaultDeserializationContext) vanilla.getDeserializationContext()) .createInstance(vanilla.getDeserializationConfig(), null, null) .findRootValueDeserializer(vanilla.constructType(ThrowingBuilt.class)); + IllegalAccessError failure = new IllegalAccessError("simulated"); SettableStringProperty prop = new SettableStringProperty(deser.findProperty("str"), new BiConsumer() { @Override public void accept(Object bean, String v) { - throw new IllegalAccessError("simulated"); + throw failure; } }, new BiFunction() { @Override public Object apply(Object bean, Object v) { - throw new IllegalAccessError("simulated"); + throw failure; } }, ThrowingBuilder.class); toThrow = () -> new IllegalStateException("thrown by setter"); setterCalls = 0; + + JsonMappingException e = assertThrows(JsonMappingException.class, + () -> prop.setAndReturn(new ThrowingBuilder(), "foo")); + assertSame(failure, e.getCause()); try (JsonParser p = vanilla.createParser(a2q("{'str':'foo'}"))) { p.nextToken(); p.nextToken(); p.nextToken(); DefaultDeserializationContext ctxt = ((DefaultDeserializationContext) vanilla.getDeserializationContext()) .createInstance(vanilla.getDeserializationConfig(), p, null); - JsonMappingException e = assertThrows(JsonMappingException.class, + e = assertThrows(JsonMappingException.class, () -> prop.deserializeSetAndReturn(p, ctxt, new ThrowingBuilder())); - assertEquals("thrown by setter", e.getOriginalMessage()); + assertSame(failure, e.getCause()); assertNotNull(e.getLocation()); } - assertEquals(1, setterCalls); + assertEquals(0, setterCalls); } @Test diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java index 3bccd08bf..6822b53ad 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; @@ -325,11 +326,10 @@ public void testFailingLoaderOnBuilder() throws Exception } - // Setup skips this accessor, so build it the same way here to check that the - // access error the JVM really raises (from a hidden frame on Java 9+) is - // recognized as the accessor's, and falls back without running the setter twice + // Setup skips this accessor, so build it the same way here: the access error the + // JVM really raises is reported like a setter failure, and the setter never runs @Test - public void testRealAccessorFailureFallsBack() throws Throwable + public void testRealAccessorFailureIsNotRetried() throws Throwable { Class mode = Class.forName(ModeBuilderBase.class.getPackage().getName() + ".Mode"); MethodHandle setter = MethodHandles.lookup().unreflect( @@ -352,15 +352,12 @@ public void testRealAccessorFailureFallsBack() throws Throwable setter.type().parameterType(0)); Object fast = mode.getEnumConstants()[0]; - for (int i = 0; i < 2; i++) { - ModeBuilderBase.setterCalls = 0; - ModeBuilder builder = new ModeBuilder(); - assertThrows(IllegalAccessError.class, () -> thunk.apply(builder, fast)); - assertEquals(0, ModeBuilderBase.setterCalls); - assertSame(builder, prop.setAndReturn(builder, fast)); - assertEquals("FAST", builder.build().mode); - assertEquals(1, ModeBuilderBase.setterCalls); - } + ModeBuilderBase.setterCalls = 0; + ModeBuilder builder = new ModeBuilder(); + assertThrows(IllegalAccessError.class, () -> thunk.apply(builder, fast)); + JsonMappingException e = assertThrows(JsonMappingException.class, () -> prop.setAndReturn(builder, fast)); + assertEquals(IllegalAccessError.class, e.getCause().getClass()); + assertEquals(0, ModeBuilderBase.setterCalls); } private SettableBeanProperty _findProperty(Class type, String name) throws Exception From ee61de21f9be4081a3a9fd154ff1f7115dc2f9c8 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 10:56:05 -0400 Subject: [PATCH 22/34] Add release notes for #366 --- release-notes/CREDITS-2.x | 5 +++++ release-notes/VERSION-2.x | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index aa75b5d3d..3f1cf41a2 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -166,3 +166,8 @@ Apoorva Manjunath (@apoorvam) * Reported, contributed fix for #285: `JacksonBlackbirdAccess` Class access exception with Groovy Script serialization using `BlackbirdModule` (2.19.0) + +Parker Winchester (@parker89) + +* Contributed #366: (blackbird) Skip generated setters the accessor can't call + (2.23.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index aa2889592..eb3383b8d 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -25,7 +25,8 @@ Active maintainers: 2.23.0 (not yet released) -No changes since 2.22 +#366: (blackbird) Skip generated setters the accessor can't call + (contributed by Parker W) 2.22.4 (17-Oct-2026) 2.22.3 (21-Sep-2026) From c37f57c66c6f35537eeb7811cfd280fbdbe3bf9e Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 10:56:56 -0400 Subject: [PATCH 23/34] Add release notes for #363 and #367 --- release-notes/CREDITS-2.x | 5 +++++ release-notes/VERSION-2.x | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index e7f43c3c8..2b7c77fd9 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -169,7 +169,12 @@ Apoorva Manjunath (@apoorvam) Parker Winchester (@parker89) +* Contributed fix for #363: (blackbird) `@JsonSetter(nulls = Nulls.SKIP)` ignored + for properties of beans with a properties-based creator + (2.23.0) * Contributed #364: (blackbird) Use generated setters for Builder-based deserialization (2.23.0) * Contributed #366: (blackbird) Skip generated setters the accessor can't call (2.23.0) +* Contributed #367: (blackbird) Don't retry failed bean setters through reflection + (2.23.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 2bdf761b1..ea5ca5139 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -25,10 +25,15 @@ Active maintainers: 2.23.0 (not yet released) +#363: (blackbird) `@JsonSetter(nulls = Nulls.SKIP)` ignored for properties of + beans with a properties-based creator + (reported by @jkuipers, fix contributed by Parker W) #364: (blackbird) Use generated setters for Builder-based deserialization (contributed by Parker W) #366: (blackbird) Skip generated setters the accessor can't call (contributed by Parker W) +#367: (blackbird) Don't retry failed bean setters through reflection + (contributed by Parker W) 2.22.4 (17-Oct-2026) 2.22.3 (21-Sep-2026) From 1fc9f35324577b8a7d89b4f5d3db7a88ff887e6c Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 11:01:42 -0400 Subject: [PATCH 24/34] Skip nulls in primitive set() like MethodProperty --- .../deser/SettableBooleanProperty.java | 7 +++++ .../blackbird/deser/SettableIntProperty.java | 7 +++++ .../blackbird/deser/SettableLongProperty.java | 7 +++++ .../blackbird/deser/TestSetterFailures.java | 29 +++++++++++++++++-- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 62270b5bb..1d38d6294 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -70,6 +70,13 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object value) throws IOException { + // as in `MethodProperty.set`: skip nulls, or leave them for reflection to report + if (value == null) { + if (!_skipNulls) { + delegate.set(bean, value); + } + return; + } // not optimal (due to boxing), but better than using reflection: final boolean b = ((Boolean) value).booleanValue(); try { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index f837120c6..c57f94842 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -64,6 +64,13 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object value) throws IOException { + // as in `MethodProperty.set`: skip nulls, or leave them for reflection to report + if (value == null) { + if (!_skipNulls) { + delegate.set(bean, value); + } + return; + } // not optimal (due to boxing), but better than using reflection: int v = ((Number) value).intValue(); try { diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 5915723f3..31ca89fcc 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -63,6 +63,13 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object value) throws IOException { + // as in `MethodProperty.set`: skip nulls, or leave them for reflection to report + if (value == null) { + if (!_skipNulls) { + delegate.set(bean, value); + } + return; + } // not optimal (due to boxing), but better than using reflection: final long v = ((Number) value).longValue(); try { diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java index b1aa6eff1..ea5fd5afc 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -82,10 +82,22 @@ static class SkipNullsBean { final int id; String str = "default"; List list = Arrays.asList("keep"); + int i = 7; + long l = 8L; + boolean b = true; @JsonCreator SkipNullsBean(@JsonProperty("id") int id) { this.id = id; } + @JsonSetter(nulls=Nulls.SKIP) + public void setI(int value) { calls++; i = value; } + + @JsonSetter(nulls=Nulls.SKIP) + public void setL(long value) { calls++; l = value; } + + @JsonSetter(nulls=Nulls.SKIP) + public void setB(boolean value) { calls++; b = value; } + @JsonSetter(nulls=Nulls.SKIP) public void setStr(String value) { calls++; str = value; } @@ -246,7 +258,7 @@ public void testAccessErrorsAreReportedAsVanilla() throws Exception { @Test public void testSetSkipsNulls() throws Exception { - for (String name : new String[] { "str", "list" }) { + for (String name : new String[] { "str", "list", "i", "l", "b" }) { SettableBeanProperty property = property(newObjectMapper(), SkipNullsBean.class, name); assertTrue(property instanceof OptimizedSettableBeanProperty, name); SkipNullsBean bean = new SkipNullsBean(1); @@ -255,14 +267,27 @@ public void testSetSkipsNulls() throws Exception { assertEquals(0, calls, name); } // with the nulls before the creator property, so they're buffered - String json = a2q("{'str':null,'list':null,'id':1}"); + String json = a2q("{'str':null,'list':null,'i':null,'l':null,'b':null,'id':1}"); for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), newObjectMapper() }) { calls = 0; SkipNullsBean bean = mapper.readValue(json, SkipNullsBean.class); assertEquals("default", bean.str); assertEquals(Arrays.asList("keep"), bean.list); + assertEquals(7, bean.i); + assertEquals(8L, bean.l); + assertTrue(bean.b); assertEquals(0, calls); } + // and without skipping, a null for a primitive fails as it does through reflection + alwaysThrow = new IllegalStateException("not reached"); + calls = 0; + Throwable expected = assertThrows(Throwable.class, + () -> property(newVanillaJSONMapper(), AlwaysFailingBean.class, "i").set(new AlwaysFailingBean(), null)); + Throwable actual = assertThrows(Throwable.class, + () -> property(newObjectMapper(), AlwaysFailingBean.class, "i").set(new AlwaysFailingBean(), null)); + assertEquals(expected.getClass(), actual.getClass()); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(0, calls); } @Test From 2806ae19765c64a87a357357b20c210599a81939 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 11:53:14 -0400 Subject: [PATCH 25/34] Don't load parameter types at setup either --- .../module/blackbird/util/ReflectionHack.java | 37 ++++-------- .../deser/TestInaccessibleSetterTypes.java | 56 +++++++++++++++++++ .../blackbird/deser/isolated/Token.java | 6 ++ .../blackbird/deser/isolated/TokenBean.java | 9 +++ .../deser/isolated/TokenBuilder.java | 10 ++++ .../blackbird/deser/isolated/TokenBuilt.java | 13 +++++ .../blackbird/deser/isolated/TokenParent.java | 21 +++++++ 7 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/Token.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBean.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilder.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilt.java create mode 100644 blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenParent.java diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java index 63b5635ff..f9f5b8d8f 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/util/ReflectionHack.java @@ -30,8 +30,10 @@ public static Lookup privateLookupIn(Class lookup, MethodHandles.Lookup orig) /** * Whether code defined with {@code lookup}, such as a lambda it spins, can use - * {@code type} where it must also access it, as in a cast. If not, that code - * fails when it runs, not when it is created. + * {@code type} where it must also access it, as in a cast: it must be the same + * class from the lookup class's loader (see {@link #isSameClassWithoutLoading}) + * and accessible from it. If not, that code fails when it runs, not when it is + * created. */ public static boolean canAccessClass(Lookup lookup, Class type) { type = elementType(type); @@ -39,7 +41,7 @@ public static boolean canAccessClass(Lookup lookup, Class type) { return true; } try { - if (!resolvesTo(lookup, type)) { + if (!isSameClassWithoutLoading(lookup, type)) { return false; } if (Java9Up.ACCESS_CLASS != null) { @@ -53,11 +55,12 @@ public static boolean canAccessClass(Lookup lookup, Class type) { } /** - * Whether {@code type}, which code defined with {@code lookup} only names in a - * descriptor (as a called method's return type) and so never loads itself, is - * known to be the same class from the lookup class's loader. Unlike - * {@link #canAccessClass} this doesn't ask that loader, which could make it - * define its own copy and break code that relies on it never doing so. + * Whether {@code type}, as code defined with {@code lookup} refers to it, is known + * to be the same class from the lookup class's loader, without asking that loader. + * Asking could make it define its own copy, which can't be undone and breaks code + * that relies on it never doing so. So only a class that loader defined itself + * qualifies, or a {@code java.*} one (which only the JDK's loaders can define); + * types from other loaders, even JDK {@code javax.*} ones, don't. */ public static boolean isSameClassWithoutLoading(Lookup lookup, Class type) { type = elementType(type); @@ -78,24 +81,6 @@ private static Class elementType(Class type) { return type; } - // That code refers to `type` by name, resolved by the lookup class's loader, - // which may find a different class of the same name (e.g. a child-first loader - // with its own copy) or none at all - private static boolean resolvesTo(Lookup lookup, Class type) { - ClassLoader loader = lookup.lookupClass().getClassLoader(); - // a class its loader defined is what that loader resolves its name to, - // without asking it to load anything - if (type.getClassLoader() == loader) { - return true; - } - try { - return Class.forName(type.getName(), false, loader) == type; - } catch (ClassNotFoundException | LinkageError | RuntimeException e) { - // including whatever else a misbehaving or closed loader throws - return false; - } - } - // Without modules, public classes are accessible from anywhere, others only // from the same runtime package (which includes the bean's own class, often // package-private). `getModifiers` reports the declared modifiers of nested diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java index 16ef6bba4..269cfa4aa 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -19,6 +19,10 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdModule; import com.fasterxml.jackson.module.blackbird.BlackbirdTestBase; +import com.fasterxml.jackson.module.blackbird.deser.isolated.Token; +import com.fasterxml.jackson.module.blackbird.deser.isolated.TokenBean; +import com.fasterxml.jackson.module.blackbird.deser.isolated.TokenBuilder; +import com.fasterxml.jackson.module.blackbird.deser.isolated.TokenBuilt; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.HiddenSetterBeanBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ModeBeanBase; import com.fasterxml.jackson.module.blackbird.deser.otherpackage.ProtectedSetterBeanBase; @@ -45,6 +49,20 @@ static final class ProtectedBean extends ProtectedSetterBeanBase { } static final class HiddenSetterBean extends HiddenSetterBeanBase { } + // Setters with JDK `java.*` signatures keep the generated setter; other types from + // other loaders, even JDK `javax.*` ones, can't be known to be the same class + static class CommonSignatures + { + String a; + List b; + int c; + + public Object setA(String v) { a = v; return this; } + public CharSequence setB(List v) { b = v; return "x"; } + public Integer setC(int v) { c = v; return v; } + public javax.naming.Name setD(String v) { return null; } + } + private final ObjectMapper MAPPER = newObjectMapper(); @Test @@ -168,6 +186,44 @@ public void testDeclaringClassIsNotLoaded() throws Exception } } + // Nor may the parameter check load a type from another loader: the property falls + // back to reflection either way, but the copy it would define can't be undone, and + // the code relying on that loader then fails + @Test + public void testParameterTypeIsNotLoaded() throws Exception + { + for (ObjectMapper mapper : new ObjectMapper[] { newVanillaJSONMapper(), MAPPER }) { + ChildFirstLoader loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Token.class, TokenBuilder.class, TokenBuilt.class)); + Class built = Class.forName(TokenBuilt.class.getName(), true, loader); + Object result = mapper.readValue(a2q("{'token':'YES'}"), built); + // the builder's own call to the inherited setter still works + assertEquals(2, built.getField("calls").get(result)); + assertFalse(loader.defined.contains(Token.class.getName())); + + loader = new ChildFirstLoader(getClass().getClassLoader(), + Arrays.asList(Token.class, TokenBean.class)); + Class bean = Class.forName(TokenBean.class.getName(), true, loader); + // the bean's constructor calls the inherited method, with no setter called at all + mapper.readValue("{}", bean); + assertFalse(loader.defined.contains(Token.class.getName())); + } + } + + @Test + public void testCommonSignaturesStayOptimized() throws Exception + { + for (String name : new String[] { "a", "b", "c" }) { + SettableBeanProperty prop = _findProperty(CommonSignatures.class, name); + assertTrue(prop instanceof OptimizedSettableBeanProperty, name); + } + assertEquals(MethodProperty.class, _findProperty(CommonSignatures.class, "d").getClass()); + CommonSignatures result = MAPPER.readValue(a2q("{'a':'x','b':['y'],'c':3,'d':'z'}"), CommonSignatures.class); + assertEquals("x", result.a); + assertEquals(Arrays.asList("y"), result.b); + assertEquals(3, result.c); + } + // A loader that fails (other than not finding the class) only stops the optimization @Test public void testFailingLoader() throws Exception diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/Token.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/Token.java new file mode 100644 index 000000000..ddc71dc3b --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/Token.java @@ -0,0 +1,6 @@ +package com.fasterxml.jackson.module.blackbird.deser.isolated; + +// These fixtures have their own package: on JDK 9 to 13 Blackbird reuses a +// `$$JacksonBlackbirdAccess` class it finds through the child loader, and other tests +// leave one in the parent loader for their own packages +public enum Token { YES } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBean.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBean.java new file mode 100644 index 000000000..a813ca829 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBean.java @@ -0,0 +1,9 @@ +package com.fasterxml.jackson.module.blackbird.deser.isolated; + +// its constructor calls the inherited method +public class TokenBean extends TokenParent +{ + public TokenBean() { + token(); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilder.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilder.java new file mode 100644 index 000000000..bcf922b18 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilder.java @@ -0,0 +1,10 @@ +package com.fasterxml.jackson.module.blackbird.deser.isolated; + +// calls the inherited method, which the JVM checks against the child loader's `Token` +public class TokenBuilder extends TokenParent +{ + public TokenBuilt build() { + withToken(null); + return new TokenBuilt(calls); + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilt.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilt.java new file mode 100644 index 000000000..3ae1a34d6 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenBuilt.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.module.blackbird.deser.isolated; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder=TokenBuilder.class) +public class TokenBuilt +{ + public final int calls; + + TokenBuilt(int calls) { + this.calls = calls; + } +} diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenParent.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenParent.java new file mode 100644 index 000000000..eb0352fa8 --- /dev/null +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/isolated/TokenParent.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.module.blackbird.deser.isolated; + +// `Token` comes from the parent loader through the inherited setter, but the child +// loader would define its own copy if asked; the code using it relies on it not +public class TokenParent +{ + public int calls; + + public Object withToken(Token t) { + calls++; + return this; + } + + public void setToken(Token t) { + calls++; + } + + public Token token() { + return null; + } +} From 14d5816f20ab1051451ad391777541d3abb4f618 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 12:01:21 -0400 Subject: [PATCH 26/34] Test builder null handling, null results and access for every property type --- .../module/blackbird/deser/TestBuilders.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index c38827281..d56b08b1d 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -6,7 +6,9 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -30,6 +32,7 @@ import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.impl.MethodProperty; +import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -617,6 +620,67 @@ public ItemBuilt build() { @JsonDeserialize(builder=ItemBuilder.class) static class ItemBuilt { } + // Setters that return null keep the current builder, for every property type + static final class NullReturningAllBuilder + { + long l; + boolean b; + List list; + + public NullReturningAllBuilder withL(long v) { l = v; return null; } + public NullReturningAllBuilder withB(boolean v) { b = v; return null; } + public NullReturningAllBuilder withList(List v) { list = v; return null; } + + public NullReturningAllBuilt build() { return new NullReturningAllBuilt(l, b, list); } + } + + @JsonDeserialize(builder=NullReturningAllBuilder.class) + static class NullReturningAllBuilt + { + final long l; + final boolean b; + final List list; + + NullReturningAllBuilt(long l, boolean b, List list) { + this.l = l; + this.b = b; + this.list = list; + } + } + + // The null handling `deserializeSetAndReturn` does for an Object setter, as + // `MethodProperty` does + static final class NullsBuilder + { + List empty = Arrays.asList("unset"); + List skip = Arrays.asList("keep"); + int calls; + + @JsonSetter(nulls=Nulls.AS_EMPTY) + public NullsBuilder withEmpty(List v) { calls++; empty = v; return this; } + + @JsonSetter(nulls=Nulls.SKIP) + public NullsBuilder withSkip(List v) { calls++; skip = v; return this; } + + public NullsBuilt build() { return new NullsBuilt(this); } + } + + @JsonDeserialize(builder=NullsBuilder.class) + static class NullsBuilt + { + final String result; + + NullsBuilt(NullsBuilder b) { + result = "empty=" + b.empty + " skip=" + b.skip + " calls=" + b.calls; + } + } + + // A public setter of a non-public builder, which Jackson makes accessible + static final class PackagePrivateBuilder + { + public PackagePrivateBuilder withValue(int v) { return this; } + } + static class Values { final String str; final List list; @@ -955,6 +1019,69 @@ public void testSetAndReturnWithUnusableBuilder() throws Exception // The rule the generated builder setters follow is the one Jackson uses to make // setters accessible, including leaving public JDK setters alone @SuppressWarnings("deprecation") + @Test + public void testNullReturningBuilderForAllTypes() throws Exception + { + for (String name : new String[] { "l", "b", "list" }) { + assertTrue(_findProperty(MAPPER, NullReturningAllBuilt.class, name) instanceof OptimizedSettableBeanProperty, + name); + } + NullReturningAllBuilt v = MAPPER.readValue(a2q("{'l':7,'b':true,'list':['x']}"), NullReturningAllBuilt.class); + assertEquals(7L, v.l); + assertTrue(v.b); + assertEquals(Arrays.asList("x"), v.list); + // and through setAndReturn + NullReturningAllBuilder builder = new NullReturningAllBuilder(); + assertSame(builder, _findProperty(MAPPER, NullReturningAllBuilt.class, "l").setAndReturn(builder, 3L)); + assertSame(builder, _findProperty(MAPPER, NullReturningAllBuilt.class, "b").setAndReturn(builder, true)); + assertSame(builder, _findProperty(MAPPER, NullReturningAllBuilt.class, "list") + .setAndReturn(builder, Collections.singletonList("y"))); + } + + @Test + public void testBuilderNullHandlingMatchesVanilla() throws Exception + { + ObjectMapper vanilla = JsonMapper.builder() + .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT).build(); + ObjectMapper blackbird = mapperBuilder() + .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT).build(); + for (String name : new String[] { "empty", "skip" }) { + assertTrue(_findProperty(blackbird, NullsBuilt.class, name) instanceof OptimizedSettableBeanProperty, name); + } + // explicit nulls, and empty Strings the deserializer turns into nulls + for (String json : new String[] { a2q("{'empty':null,'skip':null}"), a2q("{'empty':'','skip':''}") }) { + String expected = vanilla.readValue(json, NullsBuilt.class).result; + assertEquals("empty=[] skip=[keep] calls=1", expected, json); + assertEquals(expected, blackbird.readValue(json, NullsBuilt.class).result, json); + } + } + + // `setAndReturn` with a null primitive fails as reflection does, unless nulls are skipped + @Test + public void testSetAndReturnNullPrimitives() throws Exception + { + ObjectMapper vanilla = newVanillaJSONMapper(); + for (String name : new String[] { "i", "l", "b" }) { + SettableBeanProperty expectedProperty = _findProperty(vanilla, name); + SettableBeanProperty actualProperty = _findProperty(MAPPER, name); + assertTrue(actualProperty instanceof OptimizedSettableBeanProperty, name); + MutableBuilder expectedBuilder = new MutableBuilder(); + MutableBuilder actualBuilder = new MutableBuilder(); + JsonMappingException expected = assertThrows(JsonMappingException.class, + () -> expectedProperty.setAndReturn(expectedBuilder, null), name); + JsonMappingException actual = assertThrows(JsonMappingException.class, + () -> actualProperty.setAndReturn(actualBuilder, null), name); + assertEquals(expected.getOriginalMessage(), actual.getOriginalMessage(), name); + assertEquals(expected.getCause().getClass(), actual.getCause().getClass(), name); + assertEquals(0, actualBuilder.reflectiveCalls, name); + + SettableBeanProperty skipping = actualProperty.withNullProvider(NullsConstantProvider.skipper()); + assertTrue(skipping instanceof OptimizedSettableBeanProperty, name); + assertSame(actualBuilder, skipping.setAndReturn(actualBuilder, null), name); + assertEquals(0, actualBuilder.reflectiveCalls, name); + } + } + @Test public void testIsMadeAccessibleMatchesJackson() throws Exception { @@ -969,6 +1096,7 @@ public void testIsMadeAccessibleMatchesJackson() throws Exception javax.naming.CompositeName.class.getMethod("add", String.class), PublicSetterBuilder.class.getMethod("withValue", int.class), ProtectedSetterBuilder.class.getDeclaredMethod("withValue", int.class), + PackagePrivateBuilder.class.getMethod("withValue", int.class), }; for (Method setter : setters) { // as `BeanDeserializerBuilder` and `MethodProperty.fixAccess` do From 196b7252c86393582100c8c9008fde9c16789d7a Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 12:03:14 -0400 Subject: [PATCH 27/34] Hand primitive nulls straight to the delegate, and test them for every type --- .../deser/SettableBooleanProperty.java | 6 ++-- .../blackbird/deser/SettableIntProperty.java | 6 ++-- .../blackbird/deser/SettableLongProperty.java | 6 ++-- .../blackbird/deser/TestSetterFailures.java | 30 ++++++++++++------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 1d38d6294..0968ceadc 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -70,11 +70,9 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object value) throws IOException { - // as in `MethodProperty.set`: skip nulls, or leave them for reflection to report + // the delegate skips them, or reports them, as `MethodProperty.set` does if (value == null) { - if (!_skipNulls) { - delegate.set(bean, value); - } + delegate.set(bean, value); return; } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index c57f94842..42208578f 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -64,11 +64,9 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object value) throws IOException { - // as in `MethodProperty.set`: skip nulls, or leave them for reflection to report + // the delegate skips them, or reports them, as `MethodProperty.set` does if (value == null) { - if (!_skipNulls) { - delegate.set(bean, value); - } + delegate.set(bean, value); return; } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 31ca89fcc..fc3df853d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -63,11 +63,9 @@ public Object deserializeSetAndReturn(JsonParser p, @Override public void set(Object bean, Object value) throws IOException { - // as in `MethodProperty.set`: skip nulls, or leave them for reflection to report + // the delegate skips them, or reports them, as `MethodProperty.set` does if (value == null) { - if (!_skipNulls) { - delegate.set(bean, value); - } + delegate.set(bean, value); return; } // not optimal (due to boxing), but better than using reflection: diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java index ea5fd5afc..0beaaa0e6 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -278,16 +278,26 @@ public void testSetSkipsNulls() throws Exception { assertTrue(bean.b); assertEquals(0, calls); } - // and without skipping, a null for a primitive fails as it does through reflection - alwaysThrow = new IllegalStateException("not reached"); - calls = 0; - Throwable expected = assertThrows(Throwable.class, - () -> property(newVanillaJSONMapper(), AlwaysFailingBean.class, "i").set(new AlwaysFailingBean(), null)); - Throwable actual = assertThrows(Throwable.class, - () -> property(newObjectMapper(), AlwaysFailingBean.class, "i").set(new AlwaysFailingBean(), null)); - assertEquals(expected.getClass(), actual.getClass()); - assertEquals(expected.getMessage(), actual.getMessage()); - assertEquals(0, calls); + } + + // Without skipping, a null for a primitive fails as it does through reflection + @Test + public void testSetNullPrimitiveWithoutSkipping() throws Exception { + ObjectMapper vanilla = newVanillaJSONMapper(); + ObjectMapper blackbird = newObjectMapper(); + for (String name : new String[] { "i", "l", "b" }) { + SettableBeanProperty expectedProperty = property(vanilla, VoidBean.class, name); + SettableBeanProperty actualProperty = property(blackbird, VoidBean.class, name); + assertTrue(actualProperty instanceof OptimizedSettableBeanProperty, name); + calls = 0; + JsonMappingException expected = assertThrows(JsonMappingException.class, + () -> expectedProperty.set(new VoidBean(), null), name); + JsonMappingException actual = assertThrows(JsonMappingException.class, + () -> actualProperty.set(new VoidBean(), null), name); + assertEquals(expected.getOriginalMessage(), actual.getOriginalMessage(), name); + assertEquals(expected.getCause().getClass(), actual.getCause().getClass(), name); + assertEquals(0, calls, name); + } } @Test From c7bf80710caf9c03772bcaab47bec21a0f388525 Mon Sep 17 00:00:00 2001 From: Parker Winchester Date: Thu, 29 Oct 2026 13:31:26 -0400 Subject: [PATCH 28/34] Tidy TestBuilders --- .../jackson/module/blackbird/deser/TestBuilders.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index d56b08b1d..8f2df213e 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -6,7 +6,6 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Array; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -1016,9 +1015,6 @@ public void testSetAndReturnWithUnusableBuilder() throws Exception } } - // The rule the generated builder setters follow is the one Jackson uses to make - // setters accessible, including leaving public JDK setters alone - @SuppressWarnings("deprecation") @Test public void testNullReturningBuilderForAllTypes() throws Exception { @@ -1082,6 +1078,9 @@ public void testSetAndReturnNullPrimitives() throws Exception } } + // The rule the generated builder setters follow is the one Jackson uses to make + // setters accessible, including leaving public JDK setters alone + @SuppressWarnings("deprecation") @Test public void testIsMadeAccessibleMatchesJackson() throws Exception { From aafcf37dfeac28e7b15f78faa79918e215aba1d3 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 30 Oct 2026 09:02:55 -0700 Subject: [PATCH 29/34] Blackbird: report builder setter failures the same way from set() databind also calls set() on builders, for properties buffered before a property-based creator and for external type ids, so whether a setter failure was retried, how it was reported, and whether access modifiers were honored depended on the order of properties in the JSON. - Builder properties now report failures from set() like setAndReturn(): no retry through reflection, wrapped as MethodProperty does - A builder setter Jackson wouldn't make accessible is left to reflection entirely, not just for setAndReturn() - Primitive setAndReturn() checks for a generated setter before unboxing Regular beans keep their existing behavior (see TestAccessFallback). Co-Authored-By: Claude Opus 5.5 --- .../deser/BBDeserializerModifier.java | 31 +++-- .../deser/OptimizedSettableBeanProperty.java | 27 +++- .../deser/SettableBooleanProperty.java | 20 ++- .../blackbird/deser/SettableIntProperty.java | 20 ++- .../blackbird/deser/SettableLongProperty.java | 20 ++- .../deser/SettableObjectProperty.java | 14 +- .../deser/SettableStringProperty.java | 14 +- .../module/blackbird/deser/TestBuilders.java | 122 +++++++++++++++++- .../deser/TestInaccessibleSetterTypes.java | 2 +- 9 files changed, 211 insertions(+), 59 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java index 58652e5e1..9a1e14be4 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/BBDeserializerModifier.java @@ -86,8 +86,10 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, return builder; } lookup = _accessGrant.apply(lookup); + // databind sets the build method of a builder's deserializer before this + final boolean forBuilder = (builder.getBuildMethod() != null); List> newProps = findOptimizableProperties( - lookup, config, builder.getProperties()); + lookup, config, builder.getProperties(), forBuilder); // and if we found any, create mutator proxy, replace property objects if (!newProps.isEmpty()) { for (OptimizedSettableBeanProperty prop : newProps) { @@ -127,13 +129,20 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, protected List> findOptimizableProperties( Lookup lookup, DeserializationConfig config, Iterator propIterator) + { + return findOptimizableProperties(lookup, config, propIterator, false); + } + + protected List> findOptimizableProperties( + Lookup lookup, DeserializationConfig config, + Iterator propIterator, boolean forBuilder) { ArrayList> newProps = new ArrayList>(); // Ok, then, find any properties for which we could generate accessors while (propIterator.hasNext()) { try { - nextProperty(propIterator.next(), lookup, config, newProps); + nextProperty(propIterator.next(), lookup, config, forBuilder, newProps); } catch (Throwable e) { if (e instanceof Error) { throw (Error) e; @@ -149,7 +158,7 @@ protected List> findOptimizableProperties( @SuppressWarnings("unchecked") private void nextProperty(SettableBeanProperty prop, - Lookup lookup, DeserializationConfig config, + Lookup lookup, DeserializationConfig config, boolean forBuilder, ArrayList> newProps) throws Throwable { AnnotatedMember member = prop.getMember(); @@ -199,8 +208,12 @@ private void nextProperty(SettableBeanProperty prop, // Builder setters used to go through reflection, which applies Java's // access rules (modules included) unless Jackson made the setter accessible; // the generated setter ignores those rules, so it may only call what - // reflection could + // reflection could. A builder's setter is left to reflection altogether, + // as databind also calls `set()` on builders optimizeBuilderSetter = isMadeAccessible(config, method); + if (forBuilder && !optimizeBuilderSetter) { + return; + } } else { return; //setter = lookup.unreflectGetter((Field) jdkMember); @@ -214,30 +227,30 @@ private void nextProperty(SettableBeanProperty prop, newProps.add(new SettableIntProperty(prop, createSetter(lookup, ObjIntConsumer.class, ObjIntBiFunction.class, INT_TRAMPOLINE, int.class, setter, thunk), - optimizeBuilderSetter ? thunk : null, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType, forBuilder)); } else if (type == Long.TYPE) { ObjLongBiFunction thunk = createThunk(lookup, ObjLongBiFunction.class, long.class, setter); newProps.add(new SettableLongProperty(prop, createSetter(lookup, ObjLongConsumer.class, ObjLongBiFunction.class, LONG_TRAMPOLINE, long.class, setter, thunk), - optimizeBuilderSetter ? thunk : null, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType, forBuilder)); } else if (type == Boolean.TYPE) { ObjBooleanBiFunction thunk = createThunk(lookup, ObjBooleanBiFunction.class, boolean.class, setter); newProps.add(new SettableBooleanProperty(prop, createSetter(lookup, ObjBooleanConsumer.class, ObjBooleanBiFunction.class, BOOLEAN_TRAMPOLINE, boolean.class, setter, thunk), - optimizeBuilderSetter ? thunk : null, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType, forBuilder)); } } else { BiFunction thunk = createThunk(lookup, BiFunction.class, Object.class, setter); if (type == String.class) { newProps.add(new SettableStringProperty(prop, createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), - optimizeBuilderSetter ? thunk : null, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType, forBuilder)); } else { newProps.add(new SettableObjectProperty(prop, createSetter(lookup, BiConsumer.class, BiFunction.class, TRAMPOLINE, Object.class, setter, thunk), - optimizeBuilderSetter ? thunk : null, receiverType)); + optimizeBuilderSetter ? thunk : null, receiverType, forBuilder)); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 39006a294..970057399 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -41,17 +41,25 @@ abstract class OptimizedSettableBeanProperty _receiverType; + /** + * Whether this is a property of a builder, whose setter failures are reported + * by {@link #_reportProblemAndReturn} from every path, {@link #set} included. + */ + protected final boolean _forBuilder; + /* /********************************************************************** /* Life-cycle /********************************************************************** */ - protected OptimizedSettableBeanProperty(SettableBeanProperty src, Class receiverType) + protected OptimizedSettableBeanProperty(SettableBeanProperty src, Class receiverType, + boolean forBuilder) { super(src); _skipNulls = NullsConstantProvider.isSkipper(_nullProvider); _receiverType = receiverType; + _forBuilder = forBuilder; } // Base impl of `withName()` fine as-is: @@ -110,13 +118,21 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, /** * Helper method called when an exception is throw from mutator, to figure - * out what to do. + * out what to do. The parser is only used for a builder's error location, and + * is null when called from {@link #set}. * * @since 2.9 */ - protected void _reportProblem(Object bean, Object value, Throwable e) + protected void _reportProblem(JsonParser p, Object bean, Object value, Throwable e) throws IOException { + // databind calls `set()` on builders too (for properties buffered before a + // property-based creator, or external type ids), which must behave like + // `setAndReturn`, regardless of the order of properties in the JSON + if (_forBuilder) { + _reportProblemAndReturn(p, value, e); + return; + } if ((e instanceof IllegalAccessError) || (e instanceof SecurityException)) { _logAccessProblem(bean, e); @@ -137,13 +153,14 @@ protected void _reportProblem(Object bean, Object value, Throwable e) } /** - * Like {@link #_reportProblem}, for {@link #setAndReturn}: anything the generated + * Like {@link #_reportProblem}, for builders: anything the generated * setter throws is reported the way {@code MethodProperty} reports it, and never * retried. Telling a failure of the generated accessor itself from one thrown by * the setter would mean guessing from the stack trace, and a retry could run the * setter twice; setup instead leaves the setters it can't call to reflection. * The parser is only used for the error location, and is null outside of - * {@link #deserializeSetAndReturn}, as it is for {@code MethodProperty}. + * {@link #deserializeSetAndReturn} and {@link #deserializeAndSet}, as it is + * for {@code MethodProperty}. */ protected Object _reportProblemAndReturn(JsonParser p, Object value, Throwable e) throws IOException diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index d065d5680..dda6d41e6 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -15,16 +15,16 @@ final class SettableBooleanProperty private ObjBooleanBiFunction _optimizedSetterAndReturn; public SettableBooleanProperty(SettableBeanProperty src, ObjBooleanConsumer optimizedSetter, - ObjBooleanBiFunction optimizedSetterAndReturn, Class receiverType) + ObjBooleanBiFunction optimizedSetterAndReturn, Class receiverType, boolean forBuilder) { - super(src, receiverType); + super(src, receiverType, forBuilder); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableBooleanProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); + return new SettableBooleanProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType, _forBuilder); } /* @@ -48,7 +48,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(bean, b, e); + _reportProblem(p, bean, b, e); } } @@ -75,15 +75,16 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(bean, b, e); + _reportProblem(null, bean, b, e); } } @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // anything else (null, or another type that reflection may widen or reject), - // or an instance the generated setter can't take, is left to the delegate - if (!(value instanceof Boolean) || !_receiverType.isInstance(instance)) { + // a void setter, anything else (null, or another type that reflection may widen + // or reject), or an instance the generated setter can't take, is left to the delegate + if ((_optimizedSetterAndReturn == null) || !(value instanceof Boolean) + || !_receiverType.isInstance(instance)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: @@ -91,9 +92,6 @@ public Object setAndReturn(Object instance, Object value) throws IOException { } private Object _setAndReturn(JsonParser p, Object instance, boolean b) throws IOException { - if (_optimizedSetterAndReturn == null) { - return delegate.setAndReturn(instance, b); - } try { Object result = _optimizedSetterAndReturn.apply(instance, b); return (result == null) ? instance : result; diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index 5523be9d2..3ab13f2b2 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -16,16 +16,16 @@ final class SettableIntProperty private ObjIntBiFunction _optimizedSetterAndReturn; public SettableIntProperty(SettableBeanProperty src, ObjIntConsumer optimizedSetter, - ObjIntBiFunction optimizedSetterAndReturn, Class receiverType) + ObjIntBiFunction optimizedSetterAndReturn, Class receiverType, boolean forBuilder) { - super(src, receiverType); + super(src, receiverType, forBuilder); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableIntProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); + return new SettableIntProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType, _forBuilder); } /* @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(p, bean, v, e); } } @@ -69,15 +69,16 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(null, bean, v, e); } } @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // anything else (null, or another type that reflection may widen or reject), - // or an instance the generated setter can't take, is left to the delegate - if (!(value instanceof Integer) || !_receiverType.isInstance(instance)) { + // a void setter, anything else (null, or another type that reflection may widen + // or reject), or an instance the generated setter can't take, is left to the delegate + if ((_optimizedSetterAndReturn == null) || !(value instanceof Integer) + || !_receiverType.isInstance(instance)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: @@ -85,9 +86,6 @@ public Object setAndReturn(Object instance, Object value) throws IOException { } private Object _setAndReturn(JsonParser p, Object instance, int v) throws IOException { - if (_optimizedSetterAndReturn == null) { - return delegate.setAndReturn(instance, v); - } try { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 73a211d76..44782b2a9 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -16,16 +16,16 @@ final class SettableLongProperty private ObjLongBiFunction _optimizedSetterAndReturn; public SettableLongProperty(SettableBeanProperty src, ObjLongConsumer optimizedSetter, - ObjLongBiFunction optimizedSetterAndReturn, Class receiverType) + ObjLongBiFunction optimizedSetterAndReturn, Class receiverType, boolean forBuilder) { - super(src, receiverType); + super(src, receiverType, forBuilder); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableLongProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); + return new SettableLongProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType, _forBuilder); } /* @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(p, bean, v, e); } } @@ -68,15 +68,16 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(bean, v, e); + _reportProblem(null, bean, v, e); } } @Override public Object setAndReturn(Object instance, Object value) throws IOException { - // anything else (null, or another type that reflection may widen or reject), - // or an instance the generated setter can't take, is left to the delegate - if (!(value instanceof Long) || !_receiverType.isInstance(instance)) { + // a void setter, anything else (null, or another type that reflection may widen + // or reject), or an instance the generated setter can't take, is left to the delegate + if ((_optimizedSetterAndReturn == null) || !(value instanceof Long) + || !_receiverType.isInstance(instance)) { return delegate.setAndReturn(instance, value); } // not optimal (due to boxing), but better than using reflection: @@ -84,9 +85,6 @@ public Object setAndReturn(Object instance, Object value) throws IOException { } private Object _setAndReturn(JsonParser p, Object instance, long v) throws IOException { - if (_optimizedSetterAndReturn == null) { - return delegate.setAndReturn(instance, v); - } try { Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 41dc57cda..f150020e3 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -18,9 +18,9 @@ final class SettableObjectProperty private final Class _setterValueType; public SettableObjectProperty(SettableBeanProperty src, BiConsumer optimizedSetter, - BiFunction optimizedSetterAndReturn, Class receiverType) + BiFunction optimizedSetterAndReturn, Class receiverType, boolean forBuilder) { - super(src, receiverType); + super(src, receiverType, forBuilder); this._optimizedSetter = optimizedSetter; this._optimizedSetterAndReturn = optimizedSetterAndReturn; _setterValueType = (src.getMember() instanceof AnnotatedMethod) @@ -30,7 +30,7 @@ public SettableObjectProperty(SettableBeanProperty src, BiConsumer _optimizedSetterAndReturn; public SettableStringProperty(SettableBeanProperty src, BiConsumer optimizedSetter, - BiFunction optimizedSetterAndReturn, Class receiverType) + BiFunction optimizedSetterAndReturn, Class receiverType, boolean forBuilder) { - super(src, receiverType); + super(src, receiverType, forBuilder); _optimizedSetter = optimizedSetter; _optimizedSetterAndReturn = optimizedSetterAndReturn; } @Override protected SettableBeanProperty withDelegate(SettableBeanProperty del) { - return new SettableStringProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType); + return new SettableStringProperty(del, _optimizedSetter, _optimizedSetterAndReturn, _receiverType, _forBuilder); } /* @@ -42,7 +42,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object delegate.deserializeAndSet(p, ctxt, bean); return; } - set(bean, p.getText()); + _set(p, bean, p.getText()); } @Override @@ -62,10 +62,14 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, @Override public void set(Object bean, Object value) throws IOException { + _set(null, bean, value); + } + + private void _set(JsonParser p, Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, (String) value); } catch (Throwable e) { - _reportProblem(bean, value, e); + _reportProblem(p, bean, value, e); } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java index 8f2df213e..e9d833afb 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestBuilders.java @@ -310,6 +310,46 @@ public ThrowingBuilt build() { @JsonDeserialize(builder=ThrowingBuilder.class) static class ThrowingBuilt { } + // With a creator property, databind buffers the properties before it and calls + // `set()` instead of `setAndReturn()` for them + static final class CreatorThrowingBuilder + { + @JsonCreator + CreatorThrowingBuilder(@JsonProperty("id") String id) { } + + public CreatorThrowingBuilder withStr(String v) throws Exception { + ThrowingBuilder.fail(); + return this; + } + + public CreatorThrowingBuilder withList(List v) throws Exception { + ThrowingBuilder.fail(); + return this; + } + + public CreatorThrowingBuilder withI(int v) throws Exception { + ThrowingBuilder.fail(); + return this; + } + + public CreatorThrowingBuilder withL(long v) throws Exception { + ThrowingBuilder.fail(); + return this; + } + + public CreatorThrowingBuilder withB(boolean v) throws Exception { + ThrowingBuilder.fail(); + return this; + } + + public CreatorThrowingBuilt build() { + return new CreatorThrowingBuilt(); + } + } + + @JsonDeserialize(builder=CreatorThrowingBuilder.class) + static class CreatorThrowingBuilt { } + // Fails on the first call only, so a retry would succeed static Supplier flakyFailure; static int flakyCalls; @@ -497,6 +537,36 @@ public static final class ProtectedSetterBuilt } } + // As above, but properties before the creator property go through `set()` + public static final class ProtectedCreatorSetterBuilder + { + int value; + + @JsonCreator + public ProtectedCreatorSetterBuilder(@JsonProperty("id") int id) { } + + @JsonProperty("value") + protected ProtectedCreatorSetterBuilder withValue(int v) { + protectedCalls++; + value = v; + return this; + } + + public ProtectedCreatorSetterBuilt build() { + return new ProtectedCreatorSetterBuilt(value); + } + } + + @JsonDeserialize(builder=ProtectedCreatorSetterBuilder.class) + public static final class ProtectedCreatorSetterBuilt + { + public final int value; + + ProtectedCreatorSetterBuilt(int value) { + this.value = value; + } + } + // Its creator gives a `String`, not a `Child`, which databind accepts static class Child { @@ -856,6 +926,42 @@ public void testThrowingSetterMatchesVanilla() throws Exception } } + // The same, for properties buffered before a creator property, in either order + @Test + public void testThrowingBufferedSetterMatchesVanilla() throws Exception + { + List> failures = Arrays.asList( + () -> new IllegalStateException("unchecked"), + () -> new IOException("bad value"), + () -> new Exception("checked"), + () -> new AssertionError("error"), + () -> new SecurityException("thrown by setter"), + () -> new IllegalAccessError("thrown by setter")); + for (boolean wrap : new boolean[] { true, false }) { + ObjectMapper vanilla = JsonMapper.builder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + ObjectMapper blackbird = mapperBuilder() + .configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); + for (Supplier failure : failures) { + for (String prop : new String[] { "'str':'foo'", "'list':['a']", + "'i':42", "'l':42", "'b':true" }) { + for (String json : new String[] { a2q("{" + prop + ",'id':'a'}"), + a2q("{'id':'a'," + prop + "}") }) { + toThrow = failure; + setterCalls = 0; + Throwable expected = assertThrows(Throwable.class, + () -> vanilla.readValue(json, CreatorThrowingBuilt.class)); + Throwable actual = assertThrows(Throwable.class, + () -> blackbird.readValue(json, CreatorThrowingBuilt.class)); + String desc = "wrap=" + wrap + ", " + json + ", " + expected; + assertEquals(2, setterCalls, desc); + _assertSameFailure(desc, expected, actual); + } + } + } + } + } + // A setter in a hidden class looks like the generated accessor in a stack trace // (both are left out), but it must still run only once @Test @@ -959,7 +1065,7 @@ public Object apply(Object bean, Object v) { throw failure; } }, - ThrowingBuilder.class); + ThrowingBuilder.class, true); toThrow = () -> new IllegalStateException("thrown by setter"); setterCalls = 0; @@ -1138,6 +1244,20 @@ public void testAccessModifierOverridesMatchVanilla() throws Exception assertEquals(0, protectedCalls, desc); } + // and the same for a property buffered before the creator property + String bufferedJson = a2q("{'value':42,'id':1}"); + if (canOverride) { + assertEquals(42, blackbird.readValue(bufferedJson, ProtectedCreatorSetterBuilt.class).value, desc); + } else { + protectedCalls = 0; + Throwable expected = assertThrows(Throwable.class, + () -> vanilla.readValue(bufferedJson, ProtectedCreatorSetterBuilt.class), desc); + Throwable actual = assertThrows(Throwable.class, + () -> blackbird.readValue(bufferedJson, ProtectedCreatorSetterBuilt.class), desc); + _assertSameFailure(desc + ", " + expected, expected, actual); + assertEquals(0, protectedCalls, desc); + } + // the public one always works here, but only goes through the generated // setter if Jackson would have made it accessible PublicSetterBuilt result = blackbird.readValue(json, PublicSetterBuilt.class); diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java index 5533a25e5..d060403a2 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestInaccessibleSetterTypes.java @@ -451,7 +451,7 @@ public void testRealAccessorFailureIsNotRetried() throws Throwable BeanDeserializerBase deser = (BeanDeserializerBase) ctxt.findRootValueDeserializer( vanilla.constructType(ModeBuilt.class)); SettableObjectProperty prop = new SettableObjectProperty(deser.findProperty("mode"), unused, thunk, - setter.type().parameterType(0)); + setter.type().parameterType(0), true); Object fast = mode.getEnumConstants()[0]; ModeBuilderBase.setterCalls = 0; From 305d943e817a788bcd1c583fb53bd9e03d96a5fa Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 30 Oct 2026 14:01:31 -0700 Subject: [PATCH 30/34] Blackbird: simplify setter failure reporting, clean up tests - Drop unused `value` parameter of `_reportProblem`, and report access failures through `_reportProblemAndReturn` (so the throw is explicit) - Remove `TestAccessFallback.testDeserializeAccess`, whose trigger only matched Afterburner-generated classes and never fired for Blackbird - Run `TestSetterFailures` in a single thread, as it uses static state Co-Authored-By: Claude Opus 5.5 --- .../deser/OptimizedSettableBeanProperty.java | 19 +++++++--------- .../deser/SettableBooleanProperty.java | 4 ++-- .../blackbird/deser/SettableIntProperty.java | 4 ++-- .../blackbird/deser/SettableLongProperty.java | 4 ++-- .../deser/SettableObjectProperty.java | 2 +- .../deser/SettableStringProperty.java | 2 +- .../module/blackbird/TestAccessFallback.java | 22 ------------------- .../blackbird/deser/TestSetterFailures.java | 4 ++++ 8 files changed, 20 insertions(+), 41 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 2ae1f49a7..7462c01ab 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -113,24 +113,21 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, * * @since 2.9 */ - protected void _reportProblem(JsonParser p, Object value, Throwable e) + protected void _reportProblem(JsonParser p, Throwable e) throws IOException { // databind calls `set()` on builders too (for properties buffered before a // property-based creator, or external type ids), which must behave like - // `setAndReturn`, regardless of the order of properties in the JSON - if (_forBuilder) { + // `setAndReturn`, regardless of the order of properties in the JSON. + // Access failures are never retried either, as the setter may already have + // run (see `_reportProblemAndReturn`). These used to be retried through + // reflection, which reported the setter's failure the way `MethodProperty` + // does; keep that, rather than letting an `Error` escape unwrapped + if (_forBuilder + || (e instanceof IllegalAccessError) || (e instanceof SecurityException)) { _reportProblemAndReturn(p, e); return; } - // Never retried, as the setter may already have run (see - // `_reportProblemAndReturn`). These used to be retried through reflection, - // which reported the setter's failure the way `MethodProperty` does (`p` is - // only for the location); keep that, rather than letting an `Error` escape - // unwrapped - if ((e instanceof IllegalAccessError) || (e instanceof SecurityException)) { - _throwAsIOE(p, new InvocationTargetException(e), value); - } if (e instanceof IOException) { throw (IOException) e; } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 1bff01a29..587f179ba 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -48,7 +48,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(p, b, e); + _reportProblem(p, e); } } @@ -82,7 +82,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(null, b, e); + _reportProblem(null, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index f10b2e55a..a85fe1f0f 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(p, v, e); + _reportProblem(p, e); } } @@ -76,7 +76,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(null, v, e); + _reportProblem(null, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 67184c557..5e335e46d 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(p, v, e); + _reportProblem(p, e); } } @@ -75,7 +75,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(null, v, e); + _reportProblem(null, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 7de49bc3f..6924988d3 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -120,7 +120,7 @@ private void _set(JsonParser p, Object bean, Object v) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(p, v, e); + _reportProblem(p, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index ac590a609..384f1e0af 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -83,7 +83,7 @@ private void _set(JsonParser p, Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, (String) value); } catch (Throwable e) { - _reportProblem(p, value, e); + _reportProblem(p, e); } } diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java index b2f933524..5ff30bb00 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java @@ -30,16 +30,6 @@ public MyBean() { } public void setE(String e) { - /* 07-Mar-2015, tatu: This is bit tricky, as the exact stack trace varies - * depending on how code is generated. So right now we must get the call - * immediately from constructed class. - */ - - StackTraceElement[] elems = new Throwable().getStackTrace(); - StackTraceElement prev = elems[1]; - if (prev.getClassName().contains("Access4JacksonDeserializer")) { - throw new BogusTestError("boom!"); - } this.e = e; } @@ -66,18 +56,6 @@ public void testSerializeAccess() throws Exception assertEquals(BEAN_JSON, abMapper.writeValueAsString(new MyBean("a"))); } - @Test - public void testDeserializeAccess() throws Exception - { - ObjectMapper abMapper = newObjectMapper(); - MyBean bean = abMapper.readValue(BEAN_JSON, MyBean.class); - assertEquals("a", bean.getE()); - - // actually try again, to ensure handling works reliably - MyBean bean2 = abMapper.readValue(BEAN_JSON, MyBean.class); - assertEquals("a", bean2.getE()); - } - @Test public void testProxyAccessIssue181() throws Exception { ObjectMapper om = newObjectMapper(); diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java index bef4ef7a8..c2fa28edf 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -16,6 +16,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -38,6 +40,8 @@ import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; +// The beans record calls and failures in static fields, so tests must not run concurrently +@Execution(ExecutionMode.SAME_THREAD) public class TestSetterFailures extends BlackbirdTestBase { private static Supplier failure; From 822ab67f79f53eb6d19291bfb81daea0365c4354 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 30 Oct 2026 14:03:38 -0700 Subject: [PATCH 31/34] Updated release notes --- release-notes/VERSION-2.x | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index c450a2c70..2e46ecc63 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -27,7 +27,7 @@ Active maintainers: #363: (blackbird) `@JsonSetter(nulls = Nulls.SKIP)` ignored for properties of beans with a properties-based creator - (reported by @jkuipers, fix contributed by Parker W) + (reported by @jkuipers, contributed by Parker W) #364: (blackbird) Use generated setters for Builder-based deserialization (contributed by Parker W) #366: (blackbird) Skip generated setters and getters the accessor can't call From 383d4730f67842376dc1e2a7657bee438fcd1728 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 30 Oct 2026 14:06:13 -0700 Subject: [PATCH 32/34] Blackbird: report all bean setter failures as `MethodProperty` does Anything a generated setter throws is now wrapped and translated the way `MethodProperty` does it (into a `JsonMappingException` with location and path), not just `IllegalAccessError` and `SecurityException`; other exceptions and errors used to escape unwrapped. Co-Authored-By: Claude Opus 5.5 --- .../deser/OptimizedSettableBeanProperty.java | 48 +++++-------------- .../blackbird/deser/TestSetterFailures.java | 9 ++-- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index 7462c01ab..fbbb03014 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -32,8 +32,8 @@ abstract class OptimizedSettableBeanProperty _receiverType; /** - * Whether this is a property of a builder, whose setter failures are reported - * by {@link #_reportProblemAndReturn} from every path, {@link #set} included. + * Whether this is a property of a builder, for which {@link #set} leaves an + * instance the generated setter can't take to the delegate. */ protected final boolean _forBuilder; @@ -107,49 +107,25 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, */ /** - * Helper method called when an exception is throw from mutator, to figure - * out what to do. The parser is only used for the error location, and is null - * when called from {@link #set}. + * Helper method called when an exception is thrown from mutator: anything the + * generated setter throws is reported the way {@code MethodProperty} reports it, + * and never retried. Telling a failure of the generated accessor itself from one + * thrown by the setter would mean guessing from the stack trace, and a retry could + * run the setter twice; setup instead leaves the setters it can't call to reflection. + * The parser is only used for the error location, and is null outside of + * {@link #deserializeSetAndReturn} and {@link #deserializeAndSet}, as it is + * for {@code MethodProperty}. * * @since 2.9 */ protected void _reportProblem(JsonParser p, Throwable e) throws IOException { - // databind calls `set()` on builders too (for properties buffered before a - // property-based creator, or external type ids), which must behave like - // `setAndReturn`, regardless of the order of properties in the JSON. - // Access failures are never retried either, as the setter may already have - // run (see `_reportProblemAndReturn`). These used to be retried through - // reflection, which reported the setter's failure the way `MethodProperty` - // does; keep that, rather than letting an `Error` escape unwrapped - if (_forBuilder - || (e instanceof IllegalAccessError) || (e instanceof SecurityException)) { - _reportProblemAndReturn(p, e); - return; - } - if (e instanceof IOException) { - throw (IOException) e; - } - if (e instanceof Error) { - throw (Error) e; - } - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - // how could this ever happen? - throw new RuntimeException(e); + _reportProblemAndReturn(p, e); } /** - * Like {@link #_reportProblem}, for builders: anything the generated - * setter throws is reported the way {@code MethodProperty} reports it, and never - * retried. Telling a failure of the generated accessor itself from one thrown by - * the setter would mean guessing from the stack trace, and a retry could run the - * setter twice; setup instead leaves the setters it can't call to reflection. - * The parser is only used for the error location, and is null outside of - * {@link #deserializeSetAndReturn} and {@link #deserializeAndSet}, as it is - * for {@code MethodProperty}. + * Like {@link #_reportProblem}, for callers that need a return value. */ protected Object _reportProblemAndReturn(JsonParser p, Throwable e) throws IOException diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java index c2fa28edf..4c9bc392a 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/deser/TestSetterFailures.java @@ -233,14 +233,15 @@ public void testSetDoesNotRetrySetterFailures() throws Exception { } } - // Not retried, these are still reported as `MethodProperty` reports them (as they - // were after the retry), not as the setter's raw `Error` or exception + // Not retried, and reported as `MethodProperty` reports them, not as the setter's + // raw `Error` or exception @Test - public void testAccessErrorsAreReportedAsVanilla() throws Exception { + public void testSetterFailuresAreReportedAsVanilla() throws Exception { for (boolean wrap : new boolean[] { true, false }) { ObjectMapper vanilla = JsonMapper.builder().configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); ObjectMapper blackbird = mapperBuilder().configure(DeserializationFeature.WRAP_EXCEPTIONS, wrap).build(); - for (Throwable t : new Throwable[] { new SecurityException("se"), new IllegalAccessError("iae") }) { + for (Throwable t : new Throwable[] { new SecurityException("se"), new IllegalAccessError("iae"), + new IllegalStateException("ise"), new NoClassDefFoundError("ncdfe"), new AssertionError("ae") }) { for (String json : new String[] { a2q("{'str':'foo'}"), a2q("{'i':42}") }) { String desc = "wrap=" + wrap + ", " + t + ", " + json; alwaysThrow = t; From 8fb41e0ba18d867d29398207b4db627d71f72cdb Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 30 Oct 2026 14:16:00 -0700 Subject: [PATCH 33/34] Blackbird: merge setter failure report methods, clarify docs - `_reportProblem` now returns the `IOException` for callers to throw, replacing `_reportProblemAndReturn` - Clarify `_forBuilder` javadoc - Note in #367 release note that setter failures are reported as `JsonMappingException` Co-Authored-By: Claude Opus 5.5 --- .../deser/OptimizedSettableBeanProperty.java | 20 ++++++------------- .../deser/SettableBooleanProperty.java | 6 +++--- .../blackbird/deser/SettableIntProperty.java | 6 +++--- .../blackbird/deser/SettableLongProperty.java | 6 +++--- .../deser/SettableObjectProperty.java | 4 ++-- .../deser/SettableStringProperty.java | 4 ++-- release-notes/VERSION-2.x | 3 ++- 7 files changed, 21 insertions(+), 28 deletions(-) diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java index fbbb03014..a171f5455 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/OptimizedSettableBeanProperty.java @@ -32,8 +32,8 @@ abstract class OptimizedSettableBeanProperty _receiverType; /** - * Whether this is a property of a builder, for which {@link #set} leaves an - * instance the generated setter can't take to the delegate. + * Whether this is a property of a builder; if so, {@link #set} passes instances + * the generated setter can't accept (see {@link #_receiverType}) to the delegate. */ protected final boolean _forBuilder; @@ -107,7 +107,8 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, */ /** - * Helper method called when an exception is thrown from mutator: anything the + * Helper method called when an exception is thrown from mutator, to be thrown + * by the caller (which keeps the compiler aware it never returns): anything the * generated setter throws is reported the way {@code MethodProperty} reports it, * and never retried. Telling a failure of the generated accessor itself from one * thrown by the setter would mean guessing from the stack trace, and a retry could @@ -118,21 +119,12 @@ public abstract Object deserializeSetAndReturn(JsonParser jp, * * @since 2.9 */ - protected void _reportProblem(JsonParser p, Throwable e) - throws IOException - { - _reportProblemAndReturn(p, e); - } - - /** - * Like {@link #_reportProblem}, for callers that need a return value. - */ - protected Object _reportProblemAndReturn(JsonParser p, Throwable e) + protected IOException _reportProblem(JsonParser p, Throwable e) throws IOException { // `Method.invoke` wraps whatever the setter throws, and `MethodProperty` // translates that wrapper; doing the same keeps type, message, cause and path - throw _throwAsIOE(p, new InvocationTargetException(e)); + return _throwAsIOE(p, new InvocationTargetException(e)); } /* diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java index 587f179ba..a3a553b0b 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableBooleanProperty.java @@ -48,7 +48,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(p, e); + throw _reportProblem(p, e); } } @@ -82,7 +82,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, b); } catch (Throwable e) { - _reportProblem(null, e); + throw _reportProblem(null, e); } } @@ -104,7 +104,7 @@ private Object _setAndReturn(JsonParser p, Object instance, boolean b) throws IO Object result = _optimizedSetterAndReturn.apply(instance, b); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, e); + throw _reportProblem(p, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java index a85fe1f0f..b2f49c700 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableIntProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(p, e); + throw _reportProblem(p, e); } } @@ -76,7 +76,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(null, e); + throw _reportProblem(null, e); } } @@ -98,7 +98,7 @@ private Object _setAndReturn(JsonParser p, Object instance, int v) throws IOExce Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, e); + throw _reportProblem(p, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java index 5e335e46d..dec009043 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableLongProperty.java @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(p, e); + throw _reportProblem(p, e); } } @@ -75,7 +75,7 @@ public void set(Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(null, e); + throw _reportProblem(null, e); } } @@ -97,7 +97,7 @@ private Object _setAndReturn(JsonParser p, Object instance, long v) throws IOExc Object result = _optimizedSetterAndReturn.apply(instance, v); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, e); + throw _reportProblem(p, e); } } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java index 6924988d3..a01b97463 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableObjectProperty.java @@ -120,7 +120,7 @@ private void _set(JsonParser p, Object bean, Object v) throws IOException { try { _optimizedSetter.accept(bean, v); } catch (Throwable e) { - _reportProblem(p, e); + throw _reportProblem(p, e); } } @@ -145,7 +145,7 @@ private Object _setAndReturn(JsonParser p, Object instance, Object value) throws Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, e); + throw _reportProblem(p, e); } } diff --git a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java index 384f1e0af..b1b57b14b 100644 --- a/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java +++ b/blackbird/src/main/java/com/fasterxml/jackson/module/blackbird/deser/SettableStringProperty.java @@ -83,7 +83,7 @@ private void _set(JsonParser p, Object bean, Object value) throws IOException { try { _optimizedSetter.accept(bean, (String) value); } catch (Throwable e) { - _reportProblem(p, e); + throw _reportProblem(p, e); } } @@ -108,7 +108,7 @@ private Object _setAndReturn(JsonParser p, Object instance, Object value) throws Object result = _optimizedSetterAndReturn.apply(instance, value); return (result == null) ? instance : result; } catch (Throwable e) { - return _reportProblemAndReturn(p, e); + throw _reportProblem(p, e); } } } diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 2e46ecc63..536ae5db4 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -32,7 +32,8 @@ Active maintainers: (contributed by Parker W) #366: (blackbird) Skip generated setters and getters the accessor can't call (contributed by Parker W) -#367: (blackbird) Don't retry failed bean setters through reflection +#367: (blackbird) Don't retry failed bean setters through reflection; report + anything a setter throws as `JsonMappingException`, as plain databind does (contributed by Parker W) 2.22.4 (17-Oct-2026) From 06eb03f8cfd0c96ef1818089753ba161913fef91 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 30 Oct 2026 14:16:00 -0700 Subject: [PATCH 34/34] Blackbird: make `TestAccessFallback.testSerializeAccess` exercise the fallback The getter only failed for Afterburner-generated callers, so the test never reached the fallback writer under Blackbird. It now fails when called through the generated accessor, and verifies that the value is written through reflection, and that the writer stays on reflection. Co-Authored-By: Claude Opus 5.5 --- .../module/blackbird/TestAccessFallback.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java index 5ff30bb00..c103e3189 100644 --- a/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java +++ b/blackbird/src/test/java/com/fasterxml/jackson/module/blackbird/TestAccessFallback.java @@ -17,15 +17,19 @@ public BogusTestError(String msg) { } } + // Getter fails when called through the generated accessor, and works when + // called through reflection (the fallback writer) static class MyBean { private String e; + int optimizedCalls, reflectiveCalls; + public MyBean() { } MyBean(String e) { - setE(e); + this.e = e; } public void setE(String e) @@ -35,8 +39,14 @@ public void setE(String e) public String getE() { + // nearest of `Method.invoke` or a Blackbird frame tells which path called for (StackTraceElement elem : new Throwable().getStackTrace()) { - if (elem.getClassName().contains("Access4JacksonSerializer")) { + if (elem.getClassName().equals("java.lang.reflect.Method")) { + ++reflectiveCalls; + return e; + } + if (elem.getClassName().startsWith("com.fasterxml.jackson.module.blackbird.ser.")) { + ++optimizedCalls; throw new BogusTestError("boom!"); } } @@ -50,10 +60,17 @@ public String getE() public void testSerializeAccess() throws Exception { ObjectMapper abMapper = newObjectMapper(); - assertEquals(BEAN_JSON, abMapper.writeValueAsString(new MyBean("a"))); + MyBean bean = new MyBean("a"); + assertEquals(BEAN_JSON, abMapper.writeValueAsString(bean)); + // the generated getter failed, and the value was then written through reflection + assertEquals(1, bean.optimizedCalls); + assertEquals(1, bean.reflectiveCalls); - // actually try again, to ensure handling works reliably - assertEquals(BEAN_JSON, abMapper.writeValueAsString(new MyBean("a"))); + // after that, the writer stays on reflection + MyBean bean2 = new MyBean("a"); + assertEquals(BEAN_JSON, abMapper.writeValueAsString(bean2)); + assertEquals(0, bean2.optimizedCalls); + assertEquals(1, bean2.reflectiveCalls); } @Test