Update
Jackson can deserialize private fields as long as they have a getter (see https://www.baeldung.com/jackson-field-serializable-deserializable-or-not).
So, it turns out, in my scenario, that I don't need to deserialize RetryOptions
through the builder, I just need to be able to construct an instance of RetryOptions
that Jackson can use to populate the fields.
As I had multiple classes with this same constraint (no public constructors on a third-party class), I wrote the following method to generate ValueInstantiators
from a Supplier
lambda:
static ValueInstantiator createDefaultValueInstantiator(DeserializationConfig config, JavaType valueType, Supplier<?> creator) {
class Instantiator extends StdValueInstantiator {
public Instantiator(DeserializationConfig config, JavaType valueType) {
super(config, valueType);
}
@Override
public boolean canCreateUsingDefault() {
return true;
}
@Override
public Object createUsingDefault(DeserializationContext ctxt) {
return creator.get();
}
}
return new Instantiator(config, valueType);
}
Then I registered ValueInstantiators
for each of my classes, e.g:
var mapper = new ObjectMapper();
var module = new SimpleModule()
.addValueInstantiator(
RetryOptions.class,
createDefaultValueInstantiator(
mapper.getDeserializationConfig(),
mapper.getTypeFactory().constructType(RetryOptions.class),
() -> RetryOptions.newBuilder().validateBuildWithDefaults()
)
)
.addValueInstantiator(
ActivityOptions.class,
createDefaultValueInstantiator(
mapper.getDeserializationConfig(),
mapper.getTypeFactory().constructType(ActivityOptions.class),
() -> ActivityOptions.newBuilder().validateAndBuildWithDefaults()
)
);
mapper.registerModule(module);
No custom de/serializers are needed.
Original response
I found a way.
First, define a ValueInstantiator
for the class. The Jackson documentation strongly encourages you to extend StdValueInstantiator
.
In my scenario, I only needed the "default" (parameter-less) instantiator, so I overrode the canCreateUsingDefault
and createUsingDefault
methods.
There are other methods for creating from arguments if needed.
class RetryOptionsBuilderValueInstantiator extends StdValueInstantiator {
public RetryOptionsBuilderValueInstantiator(DeserializationConfig config, JavaType valueType) {
super(config, valueType);
}
@Override
public boolean canCreateUsingDefault() {
return true;
}
@Override
public Object createUsingDefault(DeserializationContext ctxt) throws IOException {
return RetryOptions.newBuilder();
}
}
Then I register my ValueInstantiator
with the ObjectMapper
:
var mapper = new ObjectMapper();
var module = new SimpleModule();
module.addDeserializer(RetryOptions.class, new RetryOptionsDeserializer());
module.addSerializer(RetryOptions.class, new RetryOptionsSerializer());
module.addValueInstantiator(
RetryOptions.Builder.class,
new RetryOptionsBuilderValueInstantiator(
mapper.getDeserializationConfig(),
mapper.getTypeFactory().constructType(RetryOptions.Builder.class))
);
mapper.registerModule(module);
Now I can deserialize an instance of RetryOptions
like so:
var options = RetryOptions.newBuilder()
.setInitialInterval(Duration.ofMinutes(1))
.setMaximumAttempts(7)
.setBackoffCoefficient(1.0)
.build();
var json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
var moreOptions = mapper.readValue(json, RetryOptions.class);
Note: my solution makes use of the de/serializers defined in the question - i.e. that first convert the RetryOptions
instance to a builder before serializing, then deserializing back to a builder and building to restore the instance.
End of original response