Home > database >  Get type of property in deserializer (Jackson)
Get type of property in deserializer (Jackson)

Time:05-11

I'm looking for a way to deserialize a subclass using a deserializer registered using the @JsonDeserialize annotation on the abstract super class. If there are better options I'm happy to adapt these options – but I'm not aware of any solution to this problem at the moment.

The core problem is: There is an abstract super class A:

@JsonSerialize(using = SerializerForA.class)
@JsonDeserialize(using = DeserializerForA.class)
public abstract class A {
  private String value;

  protected A(String value) {
    this.value = value;
  }
  ...
}

(The annotations are my attempt to do custom deserialization – maybe it's the wrong approach).

There are some derived classes, and A doesn't know any of the derived classes. Think about A is part of a framework and the derived classes are client code using the framework. Here are two derived classes:

public class B extends A {
  public B(String value) {
    super(value);
  }
  ...
}

and

public class C extends A {
  public C(String value) {
    super(value);
  }
  ...
}

These derived classes are used in a "container" class, e.g.:

public class MyClass {
  private B b;
  private C c;

  ...
}

And the corresponding JSON looks like this:

{
  "b": "value_of_b",
  "c": "value_of_c"
}

Writing a serializer is relatively simple:

public class SerializerForA extends JsonSerializer<A> {
  @Override
  public void serialize(A obj, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    gen.writeString(obj.getValue());
  }
}

The deserializer would look like this:

public class DeserializerForA extends JsonDeserializer<A> {
  @Override
  public A deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
    A result = ???
    return result;
  }
}

But how does the deserializer know, which type the resulting object has? Is it possible to get the class name from one of the parameters (e.g. DeserializationContext)?

There are some ways the code can be changed, if it helps. For example, a setter can be used for the value field, instead of the constructor, but I would prefer a constructor, or some factory method (public static A getInstance(String value) { ... }).


Edit (1) The deserializer should be called without any specific code automatically by the ObjectMapper, like:

ObjectMapper mapper = new ObjectMapper();
MyClass myClass = mapper.readValue(json, MyClass.class);

That means, Jackson knows the type of the container class. It also knows the type of the properties a and b. The reason to use a custom deserializer is that I need to have control over the instance creation process (basically, I want to reuse the same instance of each object for the same value – similar to an enum).


Edit (2) Changing the JSON structure is not an option. But I don't think it should be necessary. If I didn't need to have control over instance creation, the whole implementation would just work out of the box. Additional type information in the JSON should not be necessary.


Edit (3) The purpose of all of this is to implement a framework that can be used by application to create typesafe objects that are stored as JSON. Normally, I would use a Java enum for this purpose, but it is possible, that clients need to read JSON documents that are created by a new version of the framework (with new values), but the client didn't update the framework version yet.

Example:

There is a class called Currency:

public class Currency extends A {
  public static final Currency EUR = new Currency("EUR");
}

It is used like this:

public class Transaction {
  private Currency currency;
  private double amount;
}

The JSON would look like this:

{
  "currency": "EUR",
  "amount": 24.34
}

Now a new currency is added:

public class Currency extends A {
  public static final Currency EUR = new Currency("EUR");
  public static final Currency USD = new Currency("USD");
}

Clients with the new framework can produce the following JSON:

{
  "currency": "USD",
  "amount": 48.93,
}

One client didn't update to the new framework version. This client should be able to read the JSON without crashing.

CodePudding user response:

To sum up, the ObjectMapper is provided with an instance of MyClass containing one B and one C.

Jackson will call the JsonDeserializer<A> both for B and C providing the string "value_of_b" / "value_of_c" (because by reflection, it will know that B and C are instances of A and that's the only deserializer available in the context).

Considering that in the Jackson deserializer you are in a static context (you don't have any concrete instance of A in there, you're just deserializing some string text with information that allows you to create a new instance of MyClass that looks like the serialized instance that they provided you with), then I think the only option you have is to create a factory method somewhere in your code as you guessed (I'd create it directly in the A class):

public static A getInstance(String value) {
    ...
}

and then inside the deserializer, simply instantiate it from that independently on whether the serialized instance was a B or a C (cause at the end of the day, you only know A so you can't handle anything else):

public final class ADeserializer extends JsonDeserializer<A> {
    @Override
    public A deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        String value = jsonParser.getText();
        return A.getInstance(value);
    }
}

So basically each implementation will provide you with the String value that you need to create an A, and of course you will have to create a concrete basic implementation of A on your side in order to instantiate it (because you don't know what the other implementations are, and because you need it to be concrete to create an instance).

CodePudding user response:

You have to include some information during the serialization in the json. There are two ways to achieve that.

First is to enable default typing. This will add class names to your json. It will look like this:

{
  "a": [
    "A",
    {
      "value": "a"
    }
  ],
  "b": [
    "B",
    {
      "value": "b"
    }
  ]
}

You can enable it on ObjectMapper by calling activateDefaultTyping(ptv, DefaultTyping.OBJECT_AND_NON_CONCRETE)

Second one is to add per-class annotations. You can achieve that by adding those annotations to your abstract class.

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "type")
@JsonSubTypes({ 
  @Type(value = A.class, name = "a"), 
  @Type(value = B.class, name = "b") 
})

Then the serializer will produce json like this:

{
  "a": {
    "type": "a",
    "value": "value_of_a"
  }
  "b": {
    "type": "b",
    "value": "value_of_b"
  }
}

CodePudding user response:

A simple solution – that even doesn't need a lot of magic – is to use a factory method and @JsonCreator:

The base class is already known, and also the serializer:

  @JsonSerialize(using = SerializerForA.class)
  public class A {
    protected String value;

    public String getValue() {
      return value;
    }
  }

  public class SerializerForA extends JsonSerializer<A> {
    @Override
    public void serialize(A a, JsonGenerator gen, SerializerProvider serializers)
        throws IOException {
      gen.writeString(a.getValue());
    }
  }

The inherited classes need to implement a factory method each:

  public class B extends A {
    @JsonCreator
    public static B create(String value) {
      B b = new B();
      b.value = value;
      return b;
    }
  }

and

  public class C extends A {
    @JsonCreator
    public static C create(String value) {
      C c = new C();
      c.value = value;
      return c;
    }
  }

Now the following JSON is parsed successfully:

{
  "b":"This is B",
  "c":"This is C"
}

The obvious downside is, that inherited classes have to implement the factory method. I'd like to avoid that.

  • Related