6

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?

Matt Klein
  • 7,856
  • 6
  • 45
  • 46

3 Answers3

3

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()
Mafor
  • 9,668
  • 2
  • 21
  • 36
1

So this feels totally jankey, but it works, so... ¯\(ツ)

For my case, I needed to seralize/deseralize a Map<String, AttributeValue>, so I used a technique of stuffing the JSON version of the map into a "map" attribute, deserializing, and then extracting the "map" value:

import com.fasterxml.jackson.databind.ObjectMapper

// the Jackson mapper
val mapper = ObjectMapper()
    // I don't remember if this was needed, but I assume so...
    .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)
    // This is probably just to make the JSON smaller
    .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)

// to JSON
val dynamoAttributes: Map<String, AttributeValue> = ... 
val attributesAsJson = mapper.writeValueAsString(dynamoAttributes)

// read back from JSON
val result: Map<String, AttributeValue> =
    mapper.readValue(
        """{"m":$attributesAsJson}""", // stuff the JSON into a map field
        AttributeValue.serializableBuilderClass())
    .build()
    .m() // extract the "strongly" typed map
Matt Klein
  • 7,856
  • 6
  • 45
  • 46
  • What is the type of `mapper`? If it's a basic Jackson `ObjectMapper`, how did you configure it so that `dynamoAttributes` serialized cleanly, or did it Just Work? – chrylis -cautiouslyoptimistic- Apr 25 '23 at 17:22
  • Good question which I should have included. I've updated the code to include how the mapper was made. Just a plain Jackson mapper. – Matt Klein Apr 26 '23 at 09:16
  • For your other question, it just worked out of the box. This is using the `public T readValue(String content, Class valueType)` overload on `mapper` which returns an `AttributeValue.Builder` (hence the next `.build()`) and apparently the class that is returned by `AttributeValue.serializableBuilderClass()` can be created and populated by Jackson. Magic. – Matt Klein Apr 26 '23 at 09:21
-1

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.

Gordon Bean
  • 4,272
  • 1
  • 32
  • 47