Skip to content

Make RecordBuilder Jackson-friendly #229

@maff

Description

@maff

I'd like to use RecordBuilder in combination with Jackson using the builder for deserialization in order to leverage immutable collections and default values/initializers. This should work with a vanilla ObjectMapper instance without any custom mixins/configuration.

Problem description

Starting from the following test, it fails as type and properties is null as it does not use the builder at all (expected):

package com.example.model;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.soabase.recordbuilder.core.RecordBuilder;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

class RecordSerializationTest {

    @RecordBuilder
    @RecordBuilder.Options(
        useImmutableCollections = true
    )
    // @JsonDeserialize(builder = RecordSerializationTestMyTestModelBuilder.class)
    public record MyTestModel(
        String name,
        @RecordBuilder.Initializer("DEFAULT_TYPE") String type,
        Map<String, Object> properties
    ) {
        public static final String DEFAULT_TYPE = "dummy";
    }

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void deserializingModelInvokesBuilder() throws JsonProcessingException {
        final var json = """
            {
              "name" : "test"
            }
            """;

        final var model = objectMapper.readValue(json, MyTestModel.class);
        assertThat(model.name()).isEqualTo("test");
        assertThat(model.type()).isEqualTo("dummy");
        assertThat(model.properties()).isNotNull().isEmpty();
    }
}

When uncommenting the @JsonDeserialize line, it tries to use the builder, but fails as Jackson's default implementation expects builder methods to start with with:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "name" (class com.example.model.RecordSerializationTestMyTestModelBuilder), not marked as ignorable (0 known properties: ])
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 2, column: 13] (through reference chain: com.example.model.RecordSerializationTestMyTestModelBuilder["name"])

A solution to reconfigure Jackson to be aware of the builder using no prefix would be to annotate the builder class with @JsonPOJOBuilder(withPrefix = ""), but either I missed it or it is currently not possible to apply this to the generated builder class.

An ugly workaround is to enable public constructors on the builder class + to extend the generated builder with an annotated one (this makes the test pass):

    @RecordBuilder
    @RecordBuilder.Options(
        useImmutableCollections = true,
        publicBuilderConstructors = true
    )
    @JsonDeserialize(builder = MyTestModel.AnnotatedBuilder.class)
    public record MyTestModel(
        String name,
        @RecordBuilder.Initializer("DEFAULT_TYPE") String type,
        Map<String, Object> properties
    ) {
        public static final String DEFAULT_TYPE = "dummy";

        @JsonPOJOBuilder(withPrefix = "")
        public static class AnnotatedBuilder extends RecordSerializationTestMyTestModelBuilder {
        }
    }

Possible solutions

It would be great if RecordBuilder could expose a way to make this work nice together with Jackson without any workarounds or reconfiguration of the ObjectMapper. I'm aware that this affects interoperability with a third-party library but as this is a very common use-case (e.g. deserialization in Spring Boot controllers) it would be great to have built-in Jackson support directly in RecordBuilder.

I could imagine multiple ways of doing this:

  • Add an option like addJsonPOJOBuilderAnnotation and add @JsonPOJOBuilder(withPrefix = "") to the generated builder if configured (potentially auto-enable this when Jackson is found on the classpath?).
  • Kind of a workaround: provide a way to configure the setter prefix for builder methods to it can be set to with. I noticed this is already there, but not documented - created a small PR to update the docs in Add setterPrefix and enableGetters to options documentation #228. This works when setting it to with but it would be nicer to keep the builders as they are instead of introducing the prefix.
  • Provide a way to specify arbitrary annotations which should be applied to the builder (given technical feasibility, as far as I've seen there have been similar discussions before).

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions