简体   繁体   中英

Jackson: Deserialize Object to Array with Generics

I try to write a custom JsonDeserializer, but I can't figure out how to get the Generic type information of my class.

I18NProperty.class:

public class I18NProperty<T> extends ArrayList<T> {
  public static class Content<T> {
    public Locale i18n;     
    public T val;
  }
}

My desired JSON representation looks like this ( name is an instance of I18NProperty<Content<String>> ) :

{
  "name": {
    "en": "foo",
    "jp": "bar"
  }
}

Now I tried to write a JsonDeserializer and I'm able to read the field names and values, but I can't figure out how to create an instance of the generic type (in this example String ):

public static class I18NPropertyDeserializer extends StdDeserializer<I18NProperty<? extends Content<?>>> {
    protected I18NPropertyDeserializer() {
        super(I18NProperty.class);
    }

    @Override
    public I18NProperty<? extends Content<?>> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        I18NProperty<Content<?>> result = new I18NProperty<>();
        while(p.nextToken() != JsonToken.END_OBJECT) {
            String lang = p.getCurrentName();
            p.nextToken();              
-->         Object val = p.readValueAs(Object.class);

-->         I18NProperty.Content<?> c = new I18NProperty.Content<>();
            c.i18n = new Locale(lang);
-->         c.val = null;
            result.add(c);
        }
        return result;
    }
}

I marked the lines with --> where I need the Generic Type information.

This must be possible somehow, because normally I can give Jackson a arbitrary class which contains any generic fields and it will correctly deserialize it.

Thanks in advance, Benjamin

your code probably won't work because

  • public static class I18NPropertyDeserializer extends StdDeserializer<I18NProperty<? extends Content<?>>>

    affirm that the private variable of Content public T val; is also Content because I18NProperty<T> and public T val means that what ever goes between <> of I18NProperty is going to be the type of val, and you don't want to have val of type Content right!! you want it to be a simple object like String, Integer, Float ...

  • public class I18NProperty<T> extends ArrayList<T> means that I18NProperty is going to extend ArrayList<String> in case val is String ArrayList<Integer> in case val is Integer ... you get it what i mean .But what you want is that I18NProperty extends Array<Content<T>>

So i made a change in the class I18NProperty<T> to make it extends ArrayList<Content> .

For the class I18NPropertyDeserialize what i have done is i made the class generic to have information about type T of public T val; and in order to instantiate the class of type T we should have object class for that type passed to the constructor I18NPropertyDeserializer(Class<T> clazz) now we are able to instantiate the object wich is going to be put in Content.val

I18NPropertyDeserialize :

public class I18NPropertyDeserializer<T> extends
        StdDeserializer<I18NProperty<T>> {

    private Class<T> clazz;

    protected I18NPropertyDeserializer(Class<T> clazz) {
        super(I18NProperty.class);
        this.clazz = clazz;
    }

    @Override
    public I18NProperty<T> deserialize(JsonParser p,
            DeserializationContext ctxt) throws IOException,
            JsonProcessingException {
        I18NProperty<T> result = new I18NProperty<>();
        while (p.nextToken() != JsonToken.END_OBJECT) {
            String lang = p.getCurrentName();
            p.nextToken();
            T val;
            val = p.readValueAs(clazz);
            I18NProperty.Content<T> c = new I18NProperty.Content<>();
            c.i18n = new Locale(lang);
            c.val = val;
            result.add(c);
        }
        return result;
    }
}

I18NProperty

public class I18NProperty<T> extends ArrayList<I18NProperty.Content<T>> {
  public static class Content<T> {
    public Locale i18n;     
    public T val;
  }}

I think you don't even need custom JsonDeserializer to make it work. Here is how you can solve this. Create your objects with @JsonAnySetter annotation above someMethod(String, T). Something like this:

//Need shape=JsonFormat.Shape.OBJECT to suppress default array deserialization behavior
@JsonFormat(shape=JsonFormat.Shape.OBJECT) 
public class I18NProperty<T> extends ArrayList<I18NProperty.Content<T>>{

    public static class Content<T> {
        public Locale i18n;
        public T val;
    }

    @JsonAnySetter
    public void set(String key, T value) {
        Content<T> c = new Content<T>();
        c.i18n = new Locale(key);
        c.val = value;
        this.add(c);
    }
}

And you can use it like this:

private static final String s = 
    "{\"en\": \"foo\", \"jp\": \"bar\"}";
private static final String s2 = 
    "{\"en\":{\"title\":\"f\",\"id\":1},\"jp\":{\"title\":\"b\",\"id\":2}}";

ObjectMapper mapper = new ObjectMapper();

I18NProperty<String> o1 =
    mapper.readValue(s, new TypeReference<I18NProperty<String>>(){});

I18NProperty<TestObject> o2 = 
    mapper.readValue(s2, new TypeReference<I18NProperty<TestObject>>(){});

Simple and easy.

If you want to remove name part from json you provided, you can also use @JsonRootName(value = "name") annotation above I18NProperty and will need to set mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE); for your ObjectMapper. Example in git repo.

And here is full working git demo and additional examples + serialisation code: https://github.com/varren/JacksonDemo/tree/master/demo/src/main/java

日志演示

This is not generally possible due to type erasure.

Because the parameters of your parameterized types are wild carded ( ? extends Content<?> instead of T extends Content<T> ), all we know is that it is an Object, but we know nothing else.

Even assuming that you named your types and when you created your deserializer, you created and registered one for each type: new I18NPropertyDeserializer<String>() , new I18NPropertyDeserializer<OtherObject>() , etc. In your code you would just have a reference to the generic type T in each of these which is not a class object but a type reference which you cannot manipulate like an class Object.

The most naive way to accomplish what you want is to add an additional field into your JSON representation which includes type information, and then change your Content class to be non-generic and use inheritance instead (eg. StringContent extends Content, OtherContent extends Content, etc). Jackson has notes on polymorphic serialization here , and details on how to deal with generics here .

EDIT: The way that jackson handles this is through the use of a DeserializerFactory which generates a new deserializer for each parameterized type when you provide a type reference, for example:

List<String> values = MAPPER.readValue(jsonString, new TypeReference<List<String>>() {});

Will generate a new Deserializer specifically for each collection type on the fly. Then using a bunch of reflection it generates objects of the correct type. For an example of this take a look at BasicDeserializerFactory .

In your case, you could parameterize your StdDeserializer on a named type ( T ) rather than wildcards ( ? ) and then create your own DeserializerFactory which uses the TypeReference/JavaType passed in to the readValue call to return the proper deserializer for each given type. This seems like it would be a lot of work and it's not even really clear if you can plug in your own DerserializerFactory (SimpleModule.setDeserializers would get you close I think).

If you can enumerate the number of different content types that are possible (ie. Content, Content, Content, etc) then I would just go the route of trying register all of them explicitly and use the inheritance method mentioned above.

TLDR: Jackson does it for collections using lots of magic which is built into Jackson core, namely though BasicDeserializerFactory which is not pluggable (and explicitly handles parameterized Collection/Map/Array types but not other types).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM