jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

RFE: Add support for static @JsonCreator factory methods in Mixin classes

Open loesak opened this issue 7 years ago • 10 comments

Moved over from: https://github.com/FasterXML/jackson-future-ideas/issues/10

There are many situations where creating a simple mixin for a class is not feasable and creating a (de)serializer is a lot of effort. It would be nice if it were possible to create a static factory method annotated with @JsonCreator in a mixin class.

For example, take the class below (actually ran into this situation trying to create a mixin for UsernamePasswordAuthenticationToken from Spring security when trying to persist all the authentication objects needed for Spring Security OAuth).

public class Foo {

    private String field1;
    private Integer field2;
    private Double field3;
    private Boolean field4;

    public Foo(String field1, Integer field2) {
        this.field1 = field1;
        this.field2 = field2;
        this.field4 = false;
    }

    public Foo(String field1, Integer field2, Double field3) {
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = true;
    }

    public void setField1(String field1) {
        this.field1 = field1;
    }

    public String getField1() {
        return this.field1;
    }

    public void setField2(String field2) {
        this.field2 = field2;
    }

    public String getField2() {
        return this.field2;
    }

    public void setField3(String field3) {
        this.field3 = field3;
    }

    public String getField3() {
        return this.field3;
    }

    public void setField4(String field4) {
        if (field4) {
            throw new IllegalArgumentException("not allowed");
        }
        this.field4 = false;
    }

    public String getField4() {
        return this.field4;
    }

}

As you can see, the first constructor always sets the value for field4 to false and the second constructor sets the value to true. In addition, trying to set the value to true using the setter results in an exception. This particular use case isn't difficult to create a (de)serializer for but in the case of creating ones for the Spring Security OAuth classes () it is quite difficult. It would have been much easier if I could have created a factory class in the mixin to handle basic logic.

public abstract class FooMixin {

    @JsonCreator
    public static Foo factory(
            @JsonProperty("field1") String field1,
            @JsonProperty("field2") Integer field2,
            @JsonProperty("field3") Double field3,
            @JsonProperty("field4") Boolean field4) {

        if (field4 != null && field4) {
            return new Foo(field1, field2, field3);
        } else {
            return new Foo(field2, field2);
        }
    }

}

The solution wouldn't necessarily need to be in the mixin but maybe could be a special kind of deserializer?

public class MyDeserializer extends SomeKindOfBaseDeserializer {

    @JsonCreator
    public static Foo factory(
            @JsonProperty("field1") String field1,
            @JsonProperty("field2") Integer field2,
            @JsonProperty("field3") Double field3,
            @JsonProperty("field4") Boolean field4) {

        if (field4 != null && field4) {
            return new Foo(field1, field2, field3);
        } else {
            return new Foo(field2, field2);
        }
    }

}

I hope my explanation is clear.

loesak avatar Nov 12 '17 21:11 loesak

Perhaps I could rephrase this as requesting an ability to register an external Creator Class -- sort of generalization of ValueInstantiator. But whereas existing ValueInstantiator only exposes internal structures (and is pretty messy to implement programmatically) it should be something easier to implement.

Another thing to consider is the existence of support for builder pattern (see src/main/java/com/fasterxml/jackson/databind/annotation/JsonPOJOBuilder.java) which allows construction. Adding support for registration using, say, "config overrides", would probably be quite easy. But that would not be exactly like what is requested here.

So I think this is a good starting point for supporting external construction of value types with something more convenient than what is possible currently. Whether that should be related to mix-ins or not is an open question: I suspect it would probably be best not to combine these.

cowtowncoder avatar Nov 13 '17 23:11 cowtowncoder

Is there any workaround in the current version? I'm trying to create a mixin for org.threeten.extra.Interval that doesn't have a public constructor. Instances are created with Interval.of(from,to).

bcalmac avatar Feb 19 '18 19:02 bcalmac

UPDATE: I solved the problem by using the private constructor:

@JsonIgnoreProperties({ "empty", "unboundedStart", "unboundedEnd" })
@JsonAutoDetect(creatorVisibility = JsonAutoDetect.Visibility.ANY)
public abstract class IntervalMixin {

    public IntervalMixin(@JsonProperty("startInclusive") Instant startInclusive,
            @JsonProperty("endExclusinve") Instant endExclusive) {
    }
}

bcalmac avatar Feb 19 '18 20:02 bcalmac

Ability to apply @JsonCreator to external class with static factory method will allow to support some types that are define as final in some libraries, e.g. javatuples #2020 (without need to change that llibrary sources https://github.com/javatuples/javatuples/issues/3 )

public class MyTuplesDeserializer extends SomeKindOfBaseDeserializer {

    @JsonCreator
    public static Pair factory(
            @JsonProperty("field1") String field1,
            @JsonProperty("field2") Integer field2) {
            return new Pair(field1, field2);
    }

}

paulvi avatar May 04 '18 02:05 paulvi

@paulvi Just to make sure: mix-in annotations can already be attached to static factory methods without problems. What is requested here is bit more than that: ability to call methods in target class, not just associate annotations.

cowtowncoder avatar May 12 '18 00:05 cowtowncoder

I think it is useful to point out that the static factory methods must be defined in the class that is being instantiated/deserialized.

Reading the bellow comment appeared to me at first meant that I should be able to define the static factory in the mix-in as it was already implemented, which is not the case.

@paulvi Just to make sure: mix-in annotations can already be attached to static factory methods without problems. What is requested here is bit more than that: ability to call methods in target class, not just associate annotations.

ntroutman avatar Apr 16 '20 03:04 ntroutman

Here's another example of how the requested support would be useful.

jackson-datatye-guava does not currently support RangeMap ser/deser. It's not hard to write converters between RangeMap and List<RangeMapEntry>, where RangeMapEntry is a POJO with two properties, a range and a value.

static <K extends Comparable<? super K>, V>
List<RangeMapEntry<K, V>> fromRangeMap(RangeMap<K, V> rangeMap) {
    return EntryStream.of(rangeMap.asMapOfRanges()) // using StreamEx library
	.mapKeyValue((k, v) -> new RangeMapEntry<>(k, v))
	.toList();
}

static <K extends Comparable<? super K>, V>
ImmutableRangeMap<K, V> toRangeMap(List<RangeMapEntry<K, V>> entries) {
    return entries.stream()
	.collect(toImmutableRangeMap(e -> e.getRange(), e -> e.getValue()));
}

// assuming use of ParameterNamesModule here:
class RangeMapEntry<K extends Comparable<? super K>, V> {
    final Range<K> range;
    final V value;

    RangeMapEntry(Range<K> range, V value) {
	this.range = range;
	this.value = value;
    }

    public Range<K> getRange() { return range; }
    public V getValue() { return value; }
}

I'd like to be able to say something like the following. (I'm using the mixin syntax, but as @cowtowncoder said, this might be better as a distinct mechanism.)

abstract class RangeMapSerDeser<K extends Comparable<? super K>, V> {
    @JsonCreator RangeMap<K, V> deserialize(List<RangeMapEntry<K, V>> entries) {
        return RangeMapEntry.toRangeMap(entries);
    }
    @JsonValue List<RangeMapEntry<K, V>> serialize(RangeMap<K, V> rangeMap) {
        return RangeMapEntry.fromRangeMap(rangeMap);
    }
}

As things stand, it looks like I'm going to have to roll my own serializer and deserializer for RangeMap, using the ones for RangeSet as a model.

Tembrel avatar May 09 '20 15:05 Tembrel

I am also bitten by this shortcoming yesterday. My use case is as follows:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

class JacksonMixInTest {

    interface Vehicle {

        String getLicensePlate();

    }

    static class DefaultVehicle implements Vehicle {

        private final String licensePlate;

        private DefaultVehicle(String licensePlate) {
            this.licensePlate = licensePlate;
        }

        @Override
        public String getLicensePlate() {
            return licensePlate;
        }

    }

    abstract static class VehicleMixIn implements Vehicle {

        @JsonCreator
        static Vehicle create(
            @JsonProperty("licensePlate") String licensePlate) {
            return new DefaultVehicle(licensePlate);
        }

        @Override
        @JsonProperty("licensePlate")
        abstract public String getLicensePlate();

    }

    @Test
    void test_vehicle_deserialization() throws IOException {
        String json = "{\"licensePlate\": \"12-AB-CD\"}";
        ObjectMapper mapper = new ObjectMapper();
        mapper.addMixIn(Vehicle.class, VehicleMixIn.class);
        Vehicle vehicle = mapper.readValue(
            json.getBytes(StandardCharsets.UTF_8),
            Vehicle.class);
        Assertions
            .assertThat(vehicle)
            .isNotNull()
            .extracting(Vehicle::getLicensePlate)
            .isEqualTo("12-AB-CD");
    }

}

Here Vehicle and DefaultVehicle are located in a package that I don't have control of.

vy avatar Jan 21 '21 08:01 vy

I also have a need for this.

RatanRSur avatar May 24 '21 15:05 RatanRSur

I'd like to add my support specifically for @JsonCreator. I'm working on a library for JSON:API, which has some frustratingly duck-typed schema rules, but I'm trying to keep Jackson dependencies confined to a module and out of the main model dependency. By far the easiest way to deserialize the relationships object is to define a deserialization proxy for its contents and then roll it up in a creator method.

Perhaps the creator method could be wrapped into a Converter and set up in a StdDelegatingDeserializer?

chrylis avatar Sep 29 '22 21:09 chrylis