bashar
bashar

Reputation: 415

Jackson Serialization / Deserialization: Dynamic properties and fields

I use Spring MVC to drive the API of an application I am currently working with. The serialization of the API response is done via Jackson's ObjectMapper. I am faced with the following situation, we are extending a number of our objects to support UserDefinedFields (UDF) which is shown below in the abstract UserDefinedResponse. Being a SaaS solution, multiple clients have different configuration that is stored in the database for their custom fields.

The goal of this question is to be able to respond to each client with their UDF data. This would require

  1. Dynamically rename the fields customString1, customString2, ... to their corresponding UDF labels
  2. Remove undefined UDF fields (Example client uses only 2 out of the 4 fields.

Example of the abstract response

public abstract class UserDefinedResponse {
    public String customString1;
    public String customString2;
    public String customString3;
    public String customString4;
}

And response for a product that extends the UserDefinedResponse object

public class Product extends UserDefinedResponse {
    public long id;
    public String name;
    public float price;
}

And finally, assuming a client sets

Serializing Product for this customer should result in something similar to this:

{
   "id" : 1234,
   "name" : "MacBook Air",
   "price" : 1299,
   "supplier" : "Apple",
   "warehouse" : "New York warehouse"
}

Upvotes: 2

Views: 3822

Answers (2)

fps
fps

Reputation: 34460

I think you could do what you need with the help of a few Jackson annotations:

public abstract class UserDefinedResponse {

    @JsonIgnore
    public String customString1;

    @JsonIgnore
    public String customString2;

    @JsonIgnore
    public String customString3;

    @JsonIgnore
    public String customString4;

    @JsonIgnore // Remove if clientId must be serialized
    public String clientId;

    private Map<String, Object> dynamicProperties = new HashMap<>();

    @JsonAnyGetter
    public Map<String, Object> getDynamicProperties() {
        Mapper.fillDynamicProperties(this, this.dynamicProperties);
        return this.dynamicProperties;
    }

    @JsonAnySetter
    public void setDynamicProperty(String name, Object value) {
        this.dynamicProperties.put(name, value);
        Mapper.setDynamicProperty(this.dynamicProperties, name, this);
    }
}

First, annotate all the properties of your base class with @JsonIgnore, as these won't be part of the response. Then, make use of the @JsonAnyGetter annotation to flatten the dynamicProperties map, which will hold the dynamic properties. Finally, the @JsonAnySetter annotation is meant to be used by Jackson on deserialization.

The missing part is the Mapper utility class:

public abstract class Mapper<T extends UserDefinedResponse> {

    private static final Map<Class<T>, Map<String, Mapper<T>>> MAPPERS = new HashMap<>();

    static {
        // Mappers for Products
        Map<String, Mapper<Product>> productMappers = new HashMap<>();
        productMappers.put("CLIENT_1", new ProductMapperClient1());
        productMappers.put("CLIENT_2", new ProductMapperClient2());
        // etc for rest of clients
        MAPPERS.put(Product.class, productMappers);

        // Mappers for Providers
        Map<String, Mapper<Provider>> providerMappers = new HashMap<>();
        providerMappers.put("CLIENT_1", new ProviderMapperClient1());
        providerMappers.put("CLIENT_2", new ProviderMapperClient2());
        // etc for rest of clients
        MAPPERS.put(Provider.class, providerMappers);

        // etc for rest of entities 
        // (each entity needs to add specific mappers for every client)
    }

    protected Mapper() {
    }

    public static void fillDynamicProperties(T response, Map<String, Object> dynamicProperties) {
        // Get mapper for entity and client
        Mapper<T> mapper = MAPPERS.get(response.getClass()).get(response.clientId);
        // Perform entity -> map mapping
        mapper.mapFromEntity(response, dynamicProperties);
    }

    public static void setDynamicProperty(Map<String, Object> dynamicProperties, String name, T response) {
        // Get mapper for entity and client
        Mapper<T> mapper = MAPPERS.get(response.getClass()).get(response.clientId);
        // Perform map -> entity mapping
        mapper.mapToEntity(dynamicProperties, name, response);
    }

    protected abstract void mapFromEntity(T response, Map<String, Object> dynamicProperties);

    protected abstract void mapToEntity(Map<String, Object> dynamicProperties, String name, T response);
}

And for Product entity and client CLIENT_1:

public class ProductMapperClient1 extends Mapper<Product> {

    @Override
    protected void mapFromEntity(Product response, Map<String, Object> dynamicProperties) {
        // Actual mapping from Product and CLIENT_1 to map
        dynamicProperties.put("supplier", response.customString1);
        dynamicProperties.put("warehouse", response.customString2);
    }

    @Override
    protected void mapToEntity(Map<String, Object> dynamicProperties, String name, Product response) {
        // Actual mapping from map and CLIENT_1 to Product
        String property = (String) dynamicProperties.get(name);
        if ("supplier".equals(name)) {
            response.customString1 = property;
        } else if ("warehouse".equals(name)) {
            response.customString2 = property;
        }
    }
}

The idea is that there's a specific mapper for each (entity, client) pair. If you have many entities and/or clients, then you might consider filling the map of mappers dynamically, maybe reading from some config file and using reflection to read the properties of the entity.

Upvotes: 3

rfoltyns
rfoltyns

Reputation: 389

Have you considered returning Map<> as a response? Or a part of the response, like response.getUDF().get("customStringX"))? This should save you some possible trouble in the future, e.g.: 10 millions of concurrent users means 10 million classes in your VM.

Upvotes: 0

Related Questions