Using Jackson with immutable class with private Builder

Viewed 563

I'm trying to serialize/deserialize the DynamoDB V2 AttributeValue class using Jackson.

It is setup as an immutable class with a Builder and the builder has a private constructor. In order to create a builder, you need to call AttributeValue.builder().

I have no control over this class, so I want to use Jackson mixins.

I've used the @JsonDeserialize(builder = AttributeValue.Builder::class) and registered the mixin:

@JsonDeserialize(builder = AttributeValue.Builder::class)
interface AttributeValueMixin {
}

private val mapper = jacksonObjectMapper()
    .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
    .addMixIn(AttributeValue::class.java, AttributeValueMixin::class.java)

However, Jackson is trying to use the default constructor of the AttributeValue.Builder and it can't since it doesn't have one.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of software.amazon.awssdk.services.dynamodb.model.AttributeValue$Builder (no Creators, like default construct, exist)

How can I get Jackson to use the AttributeValue.builder() factory function? Or any other ideas on how to use Jackson to serialize/deserialize this AttributeValue class?

2 Answers

Tricky indeed. I can think of two solutions:

  1. Creating a wrapper around the original builder:
class BuilderDelegate {

    var field1 : String? = null
    var field2 : String? = null
    ...

    fun build() = AttributeValue.builder().also {
        it.field1 = field1
        it.field2 = field2
        ...
    }.build()
}

@JsonDeserialize(builder = BuilderDelegate::class)
interface AttributeValueMixin {
}
  1. If you are calling object mapper directly, you can try the following hack:
val builder = mapper.readerForUpdating(AttributeValue.builder())
val value = builder.readValue<AttributeValue.Builder>(jsonData).build()

See my answer for this question: https://stackoverflow.com/a/65603336/2288986

Summary

First a helper method:

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 add the ValueInstantiator for your class:

var mapper = new ObjectMapper();
var module = new SimpleModule()
        .addValueInstantiator(
                AttributeValue.Builder.class,
                createDefaultValueInstantiator(
                        mapper.getDeserializationConfig(),
                        mapper.getTypeFactory().constructType(AttributeValue.Builder.class),
                        AttributeValue::builder
                )
        );

mapper.registerModule(module);

Now Jackson will be able to instantiate an AttributeValue.Builder.

Related