single custom deserializer for all objects as thei

2019-01-26 20:00发布

@Entity
public Product {
   @Id
   public int id;

   public String name;

   @ManyToOne(cascade = {CascadeType.DETACH} )
   Category category

   @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.DETACH} )
   Set<Category> secondaryCategories;


}

and this entity:

@Entity
public Category {
   @Id
   public int id;

   public String name;
}

I would like to be able to send a POST with json

{ name: "name", category: 2, secondaryCategories: [3,4,5] } from client-side

and be able to be deserialized like:

{ name: "name", category: {id: 2 }, secondaryCategories: [{id: 3}, {id: 4}, {id: 5}] }

in case it was sent as

 { name: "name", category: {id: 2 }, secondaryCategories: [{id: 3}, {id: 4}, {id: 5}] }

I would like it to still work as now

what kind of annotation and custom deserializer I need? Hopefully the deserializer can work for all possible objects that have id as a property

Thanks!

Edit

3条回答
该账号已被封号
2楼-- · 2019-01-26 20:46

The complete solution after much struggle was - thanks to https://stackoverflow.com/users/1032167/varren's comment and https://stackoverflow.com/a/16825934/986160 I was able to use the default deserialization (through a local new objectMapper) in my StdDeserializer without the hurdles in this answer: https://stackoverflow.com/a/18405958/986160

The code tries to parse an int and if it is a whole object it just passes it through - so it still works for example when you make a POST/PUT request of a Category or in other words when Category is not embedded

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;

import java.io.IOException;

public class IdWrapperDeserializer<T> extends StdDeserializer<T> {

    private Class<T> clazz;

    public IdWrapperDeserializer(Class<T> clazz) {
        super(clazz);
        this.clazz = clazz;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper;
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        String json = jp.readValueAsTree().toString();

        T obj = null;
        int id = 0;
        try {
            id = Integer.parseInt(json);
        }
        catch( Exception e) {
            obj = objectMapper().readValue(json, clazz);
            return obj;
        }
        try {
            obj = clazz.newInstance();
            ReflectionUtils.set(obj,"id",id);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return obj;
    }

}

for each entity I need to behave like described I need to configure it in global ObjectMapper Bean of the Spring Boot application:

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
    mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

    SimpleModule testModule = new SimpleModule("MyModule")
            .addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))

    mapper.registerModule(testModule);

    return mapper;
}

This is my ReflectionUtils from https://stackoverflow.com/a/14374995/986160

public class ReflectionUtils {
    // 
    public static boolean set(Object object, String fieldName, Object fieldValue) {
        Class<?> clazz = object.getClass();
        while (clazz != null) {
            try {
                Field field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                field.set(object, fieldValue);
                return true;
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }
        return false;
    }
}
查看更多
贪生不怕死
3楼-- · 2019-01-26 20:47

There are several options you could try, actually custom deserializer/serializer would probably make sense, but you also can achieve this with @JsonIdentityInfo(for deserialization) + @JsonIdentityReference(if you need serialization as integer) annotations.


Deserialization

Work both for 
{ "category":1 }
{ "category":{ "id":1 }

So you need to annotate every class that can be deserialized from its id with @JsonIdentityInfo

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id", 
        scope = Product.class,  // different for each class
        resolver = MyObjectIdResolver.class)

The hard part here is that you actually have to write custom ObjectIdResolver that can resolve objects from your db/other source. Take a look at my simple reflection version in MyObjectIdResolver.resolveId method in example below:

private static class MyObjectIdResolver implements ObjectIdResolver {
    private Map<ObjectIdGenerator.IdKey,Object> _items  = new HashMap<>();

    @Override
    public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
        if (!_items.containsKey(id)) _items.put(id, pojo);
    }

    @Override
    public Object resolveId(ObjectIdGenerator.IdKey id) {
        Object object = _items.get(id);
        return object == null ? getById(id) : object;
    }

    protected Object getById(ObjectIdGenerator.IdKey id){
        Object object = null;
        try {
            // todo objectRepository.getById(idKey.key, idKey.scope)
            object = id.scope.getConstructor().newInstance(); // create instance
            id.scope.getField("id").set(object, id.key);  // set id
            bindItem(id, object);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return object;
    }

    @Override
    public ObjectIdResolver newForDeserialization(Object context) {
        return this;
    }

    @Override
    public boolean canUseFor(ObjectIdResolver resolverType) {
        return resolverType.getClass() == getClass();
    }
}

Serialization

Default behavior
{ "category":{ "id":1 , "name":null} , secondaryCategories: [1 , { { "id":2 , "name":null} ]}

Default behavior is described here: https://github.com/FasterXML/jackson-databind/issues/372 and will produce object for the first element and id for each element after. An ID/reference mechanism in Jackson works so that an object instance is only completely serialized once and referenced by its ID elsewhere.

Option 1. (Always as id)

Works for 
{ "category":1 , secondaryCategories:[1 , 2]}

Need to use @JsonIdentityReference(alwaysAsId = true) above each object field(can uncomment in demo at the bottom of the page)

Option 2. (Always as full object representation)

Works for 
{ "category" : { "id":1 , "name":null} , secondaryCategories: [{ "id":1 , "name":null} , { "id":2 , "name":null}]}

This option is tricky because you will have to remove all the IdentityInfo for serialization somehow. One option could be to have 2 object mappers. 1 for serialization and 2-nd for deserialization and configure some sort of mixin or @JsonView

Another approach that is easier to implement is to use SerializationConfig to ignore @JsonIdentityInfo annotations completely

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();

    SerializationConfig config = mapper.getSerializationConfig()
            .with(new JacksonAnnotationIntrospector() {
        @Override
        public ObjectIdInfo findObjectIdInfo(final Annotated ann) {
            return null;
        }
    });

    mapper.setConfig(config);

    return mapper;
}

Probably the better approach would be to actually define @JsonIdentityInfo for deserializerconfig the same way and remove all annotations above classes. Something like this

At this point you probably wish you just wrote custom serializer/deserializer


Here is working (simple Jackson without spring) demo:

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;

public class Main {

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "id",
            resolver = MyObjectIdResolver.class,
            scope = Category.class)
    public static class Category {
        @JsonProperty("id")
        public int id;
        @JsonProperty("name")
        public String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Category{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "id",
            resolver = MyObjectIdResolver.class,
            scope = Product.class)
    public static class Product {
        @JsonProperty("id")
        public int id;
        @JsonProperty("name")
        public String name;

        // Need @JsonIdentityReference only if you want the serialization
        // @JsonIdentityReference(alwaysAsId = true)
        @JsonProperty("category")
        Category category;

        // Need @JsonIdentityReference only if you want the serialization
        // @JsonIdentityReference(alwaysAsId = true)
        @JsonProperty("secondaryCategories")
        Set<Category> secondaryCategories;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "Product{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", category=" + category +
                    ", secondaryCategories=" + secondaryCategories +
                    '}';
        }
    }

    private static class MyObjectIdResolver implements ObjectIdResolver {

       private Map<ObjectIdGenerator.IdKey,Object> _items;

        @Override
        public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
            if (_items == null) {
                _items = new HashMap<ObjectIdGenerator.IdKey,Object>();
            } if (!_items.containsKey(id))
                _items.put(id, pojo);
        }

        @Override
        public Object resolveId(ObjectIdGenerator.IdKey id) {
            Object object = (_items == null) ? null : _items.get(id);
            if (object == null) {
                try {

                    // create instance
                    Constructor<?> ctor = id.scope.getConstructor();
                    object = ctor.newInstance();

                    // set id
                    Method setId = id.scope.getDeclaredMethod("setId", int.class);
                    setId.invoke(object, id.key);
                    // https://github.com/FasterXML/jackson-databind/issues/372
                    // bindItem(id, object); results in strange behavior

                } catch (NoSuchMethodException | IllegalAccessException
                        | InstantiationException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
            return object;
        }

        @Override
        public ObjectIdResolver newForDeserialization(Object context) {
            return new MyObjectIdResolver();
        }

        @Override
        public boolean canUseFor(ObjectIdResolver resolverType) {
            return resolverType.getClass() == getClass();
        }
    }

    public static void main(String[] args) throws Exception {
        String str = "{ \"name\": \"name\", \"category\": {\"id\": 2 }, " +
                "\"secondaryCategories\":[{\"id\":3},{\"id\":4},{\"id\":5}]}";

        // from  str
        Product product = new ObjectMapper().readValue(str, Product.class);
        System.out.println(product);

        // to json
        String productStr = new ObjectMapper().writeValueAsString(product);
        System.out.println(productStr);

        String str2 = "{ \"name\": \"name\", \"category\":  2, " +
                "\"secondaryCategories\": [ 3,  4,  5] }";

        // from  str2
        Product product2 = new ObjectMapper().readValue(str2, Product.class);
        System.out.println(product2);

        // to json
        String productStr2 = new ObjectMapper().writeValueAsString(product2);
        System.out.println(productStr2);
    }
}
查看更多
Juvenile、少年°
4楼-- · 2019-01-26 20:57

Another approach is to use @JsonCreator factory method if you can modify your Entity

private class Product {
    @JsonProperty("category")
    private Category category;

    @JsonProperty("secondaryCategories")
    private List<Category> secondaryCategories;
}


private class Category {
    @JsonProperty("id")
    private int id;

    @JsonCreator
    public static Category factory(int id){
        Category p = new Category();
        p.id = id;
        // or some db call 
        return p;
    }
}

Or even something like this should also work

private class Category {
    private int id;

    public Category() {}

    @JsonCreator
    public Category(int id) {
        this.id = id;
    }
}
查看更多
登录 后发表回答