Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ad62c04
Use Blackbird generated setters for builder deserialization
parker89 Oct 21, 2026
10d1b11
Add void-setter and skip-nulls builder tests
parker89 Oct 21, 2026
5b493d8
Test that builder setter failures match stock
parker89 Oct 26, 2026
86e17c9
Match stock exception handling for builder setters
parker89 Oct 26, 2026
066b09e
Only retry builder setters on accessor access failures
parker89 Oct 26, 2026
d7ef5bc
Add release notes
cowtowncoder Oct 28, 2026
eca13bb
Avoid retrying exceptions thrown by bean setters
parker89 Oct 29, 2026
330ac20
Report JSON location for builder setter failures
parker89 Oct 29, 2026
dd47beb
Skip generated setters for types the accessor can't access
parker89 Oct 29, 2026
b473c69
Leave other value types to the delegate in builder setters
parker89 Oct 29, 2026
46963b0
Match MethodProperty for builder receivers, declaring classes and ski…
parker89 Oct 29, 2026
59706d7
Handle other value types, null text and failing stack traces like Met…
parker89 Oct 29, 2026
526e2df
Keep builder setters on reflection when access modifier overrides are…
parker89 Oct 29, 2026
c231b64
Don't load setter return types at setup, and tolerate failing loaders
parker89 Oct 29, 2026
3fc8096
Only use generated builder setters where Jackson would make the sette…
parker89 Oct 29, 2026
13d411a
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
8857040
Match MethodProperty for bean setter errors, skipped nulls and null text
parker89 Oct 29, 2026
6530a02
Skip generated setters the accessor can't call
parker89 Oct 29, 2026
7c5995b
Merge remote-tracking branch 'fork/pw/blackbird-setup-checks' into pw…
parker89 Oct 29, 2026
7a7f269
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
c352ebb
Don't load the declaring class, or check unsupported setter types, at…
parker89 Oct 29, 2026
aca43ef
Don't optimize public JDK builder setters, which Jackson doesn't make…
parker89 Oct 29, 2026
e25aeb6
Merge remote-tracking branch 'fork/pw/blackbird-setup-checks' into pw…
parker89 Oct 29, 2026
32784bc
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
8d0e014
Cover polymorphic nulls for regular bean setters
parker89 Oct 29, 2026
f0ec75f
Never retry builder setters after the generated setter fails
parker89 Oct 29, 2026
849146a
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
ee61de2
Add release notes for #366
parker89 Oct 29, 2026
f358c47
Merge remote-tracking branch 'fork/pw/blackbird-setup-checks' into pw…
parker89 Oct 29, 2026
204ad4b
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
c37f57c
Add release notes for #363 and #367
parker89 Oct 29, 2026
1fc9f35
Skip nulls in primitive set() like MethodProperty
parker89 Oct 29, 2026
2806ae1
Don't load parameter types at setup either
parker89 Oct 29, 2026
14d5816
Test builder null handling, null results and access for every propert…
parker89 Oct 29, 2026
196b725
Hand primitive nulls straight to the delegate, and test them for ever…
parker89 Oct 29, 2026
4f89e04
Merge remote-tracking branch 'fork/pw/blackbird-setup-checks' into pw…
parker89 Oct 29, 2026
6cf73d8
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
c7bf807
Tidy TestBuilders
parker89 Oct 29, 2026
ab5950a
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 29, 2026
66a98f7
Merge branch '2.x' into pw/blackbird-builder-setters
cowtowncoder Oct 30, 2026
ce81126
Merge branch 'pw/blackbird-builder-setters' into pw/blackbird-setter-…
cowtowncoder Oct 30, 2026
aafcf37
Blackbird: report builder setter failures the same way from set()
cowtowncoder Oct 30, 2026
0880779
Merge remote-tracking branch 'fork/pw/blackbird-builder-setters' into…
parker89 Oct 30, 2026
e1feeb2
Merge branch '2.x' into pw/blackbird-setter-retries
cowtowncoder Oct 30, 2026
305d943
Blackbird: simplify setter failure reporting, clean up tests
cowtowncoder Oct 30, 2026
822ab67
Updated release notes
cowtowncoder Oct 30, 2026
383d473
Blackbird: report all bean setter failures as `MethodProperty` does
cowtowncoder Oct 30, 2026
8fb41e0
Blackbird: merge setter failure report methods, clarify docs
cowtowncoder Oct 30, 2026
06eb03f
Blackbird: make `TestAccessFallback.testSerializeAccess` exercise the…
cowtowncoder Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.fasterxml.jackson.core.*;

Expand All @@ -26,13 +24,6 @@ abstract class OptimizedSettableBeanProperty<T extends OptimizedSettableBeanProp
*/
final protected boolean _skipNulls;

/**
* Marker that we set once we have logged an access problem with the generated
* mutator, so it's only logged once; the mutator itself is still used, falling
* back to the standard one on each such problem.
*/
private volatile boolean loggedAccessProblem = false;

/**
* Type the generated builder setter casts the instance to: the setter's declaring
* class, or the builder's own class for an inherited protected setter. A builder
Expand All @@ -41,8 +32,8 @@ abstract class OptimizedSettableBeanProperty<T extends OptimizedSettableBeanProp
protected final Class<?> _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; if so, {@link #set} passes instances
* the generated setter can't accept (see {@link #_receiverType}) to the delegate.
*/
protected final boolean _forBuilder;

Expand Down Expand Up @@ -116,71 +107,24 @@ 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 a builder's error location, and
* is null when called from {@link #set}.
*
* @since 2.9
*/
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, e);
return;
}
if ((e instanceof IllegalAccessError)
|| (e instanceof SecurityException)) {
_logAccessProblem(bean, e);
delegate.set(bean, value);
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);
}

/**
* 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.
* 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
* 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 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));
}

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 (!loggedAccessProblem) {
loggedAccessProblem = true;
String msg = String.format("Blackbird deserialization for %s (field %s; mutator %s) falling back to reflection, due to access error (type %s, message=%s); further such errors are not logged%n",
bean.getClass(), _propName, getClass().getName(),
e.getClass().getName(), e.getMessage());
Logger.getLogger(getClass().getName()).log(Level.WARNING, msg, e);
}
}
return _throwAsIOE(p, new InvocationTargetException(e));
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object
try {
_optimizedSetter.accept(bean, b);
} catch (Throwable e) {
_reportProblem(p, bean, b, e);
throw _reportProblem(p, e);
}
}

Expand Down Expand Up @@ -82,7 +82,7 @@ public void set(Object bean, Object value) throws IOException {
try {
_optimizedSetter.accept(bean, b);
} catch (Throwable e) {
_reportProblem(null, bean, b, e);
throw _reportProblem(null, e);
}
}

Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
try {
_optimizedSetter.accept(bean, v);
} catch (Throwable e) {
_reportProblem(p, bean, v, e);
throw _reportProblem(p, e);
}
}

Expand Down Expand Up @@ -76,7 +76,7 @@ public void set(Object bean, Object value) throws IOException {
try {
_optimizedSetter.accept(bean, v);
} catch (Throwable e) {
_reportProblem(null, bean, v, e);
throw _reportProblem(null, e);
}
}

Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
try {
_optimizedSetter.accept(bean, v);
} catch (Throwable e) {
_reportProblem(p, bean, v, e);
throw _reportProblem(p, e);
}
}

Expand Down Expand Up @@ -75,7 +75,7 @@ public void set(Object bean, Object value) throws IOException {
try {
_optimizedSetter.accept(bean, v);
} catch (Throwable e) {
_reportProblem(null, bean, v, e);
throw _reportProblem(null, e);
}
}

Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -118,7 +120,7 @@ private void _set(JsonParser p, Object bean, Object v) throws IOException {
try {
_optimizedSetter.accept(bean, v);
} catch (Throwable e) {
_reportProblem(p, bean, v, e);
throw _reportProblem(p, e);
}
}

Expand All @@ -143,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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(p, bean, p.getText());
_set(p, bean, text);
}

@Override
Expand Down Expand Up @@ -80,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, bean, value, e);
throw _reportProblem(p, e);
}
}

Expand All @@ -105,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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,36 @@ 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)
{
/* 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;
}

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!");
}
}
Expand All @@ -60,22 +60,17 @@ public String getE()
public void testSerializeAccess() throws Exception
{
ObjectMapper abMapper = newObjectMapper();
assertEquals(BEAN_JSON, abMapper.writeValueAsString(new MyBean("a")));

// actually try again, to ensure handling works reliably
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());
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);

// 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
Expand Down
Loading
Loading