Make RecordBuilder Jackson-friendly
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
addJsonPOJOBuilderAnnotationand 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 https://github.com/Randgalt/record-builder/pull/228. This works when setting it towithbut 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).
Is this fixed by https://github.com/Randgalt/record-builder/pull/228?
#228 only added already existing options which were missing in the docs - it would only fix this issue when configuring record-builder to use with as setter prefix which is not ideal.
I think the easiest fix for this would be to add an option to add @JsonPOJOBuilder to the generated builders, setting the prefix to the setterPrefix configured for the record builder (could be omitted if the setterPrefix is with).
@Randgalt I created a draft PR of the first option mentioned above in https://github.com/Randgalt/record-builder/pull/232 - let me know if you'd be open to proceed adding this feature.