Iavra
Iavra

Reputation: 23

Mapping fields as key-value pair

When reading a JSON file, i would like to map my class as follows:

public class Effect {
    private final String type;
    private final Map<String, String> parameters;

    public Effect(String type, Map<String, String> parameters) {
        this.type = type;
        this.parameters = parameters;
    }

    public String getType() {
        return this.type;
    }

    public Map<String, String> getParameters() {
        return this.parameters;
    }
}

 

{
    "type": {
        "key1": "value1", 
        "key2": "value2", 
    }
}

So, the mapped JSON object consists of type as the only key and parameters as its value.

I would like to use @JsonCreator on the constructor, but can't figure out, how to map the fields. Do i need to write a custom deserializer or is there an easier way to map the class like i want?


I wrote a custom deserializer, which does what i want, but there might be an easier way, maybe with annotations alone, which i would like to know:

public class EffectDeserializer extends StdDeserializer<Effect> {
    private static final long serialVersionUID = 1L;

    public EffectDeserializer() {
        super(Effect.class);
    }

    @Override
    public Effect deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
        JsonNode node = parser.getCodec().readTree(parser);
        Iterator<String> fieldNames = node.fieldNames();
        if(fieldNames.hasNext()) {
            String type = fieldNames.next();
            Map<String, String> parameters = new HashMap<>();
            for(Iterator<Entry<String, JsonNode>> fields = node.get(type).fields(); fields.hasNext(); ) {
                Entry<String, JsonNode> field = fields.next();
                parameters.put(field.getKey(), field.getValue().textValue());
            }
            return new Effect(type, parameters);
        }
        return null;
    }
}

Another way i found would be adding a JsonCreator (constructor in this case), that takes a Map.Entry<String, Map<String, String> and uses that to initialize the values, like this:

@JsonCreator
public Effect(Map.Entry<String, Map<String, String>> entry) {
    this.type = entry.getKey();
    this.parameters = entry.getValue();
}

If there's no way to get it done with a "normal" constructor, i will probably end up using this, as it uses Jackson's default mapping for Map.Entry, reducing possible error margin.

Upvotes: 2

Views: 857

Answers (1)

shmosel
shmosel

Reputation: 50716

Add a static factory method that accepts a Map with a dynamic key:

@JsonCreator
public static Effect create(Map<String, Map<String, String>> map) {
    String type = map.keySet().iterator().next();
    return new Effect(type, map.get(type));
}

EDIT: Just noticed this is basically an uglier version of your own solution using Map.Entry. I would go with that instead.

Upvotes: 1

Related Questions