MiniScalope
MiniScalope

Reputation: 1479

how to resolve object references with a custom Jackson deserializer

{
    "name": "test",
    "columns": [
        {
            "name": "a",
            "type": "TEXT"
        },
        {
            "name": "b",
            "type": "TEXT"
        }
    ],
    "rules": [
        {
            "production": {
                "a": "[b]"
            },
            "filters": {
                "a": [
                    "",
                    "ALL",
                    false
                ]
            },
            "refcolumns": [
                "b"
            ]
        }
    ]
}

The JSON document has a property columns that contains a Set of Column objects (could also be a map using the property name as a key). This is the only spot where the columns objects are fully serialized in JSON. Everywhere else, columns are referenced using the name property that is unique

Reference can be used for map keys and values

I would like to deserialize this document and :

JsonIdentityInfodoesnt work for map keys. So I use custom serializers

Here how is serialized the Rule class, JsonColumnKeySerializer just return the "name" property of Column

class Rule {
    @JsonSerialize(keyUsing = Column.JsonColumnKeySerializer.class) 
    private HashMap<Column, RuleFormula> productions = new HashMap<>();
    @JsonSerialize(keyUsing = Column.JsonColumnKeySerializer.class) 
    private Map<Column, RuleFilter> filters = new LinkedHashMap<>();
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "name")
    @JsonIdentityReference(alwaysAsId=true) // for testing purposes...
    private Set<Column> refcolumns = new HashSet<>();
}

Upvotes: 0

Views: 1138

Answers (2)

MiniScalope
MiniScalope

Reputation: 1479

I implemented the solution proposed here Serialize and Deserialize Map<Object, Object> Jackson

I had to crawl up to the Root parent to find the data.

Compared to the I changed the Set<Column> to a Map<String,Column>.

public class JsonColumnKeyDeserializer extends com.fasterxml.jackson.databind.KeyDeserializer {
    @Override
    public Object deserializeKey(String key, DeserializationContext context) throws IOException {
        final MyRootClass root = (MyRootClass ) getRoot(context);
        final Map<String, Column> columns = root.getColumns();
        return columns.get(key);
    }

    private Object getRoot(DeserializationContext context) {
        JsonStreamContext parent = context.getParser().getParsingContext().getParent();
        while (parent.getParent() != null && !parent.getParent().inRoot()) {
            parent = parent.getParent();
        }
        return parent.getCurrentValue();
    }
}

Upvotes: 0

Chaosfire
Chaosfire

Reputation: 6995

I guess jackson should be able to do it for you, but i couldn't figure out how. As a workaround you can write custom deserializer, in which you can cache the results by name property:

public class CachingColumnDeserializer extends JsonDeserializer<Column> {

  private static final Map<String, Column> MAP = new HashMap<>();

  @Override
  public Column deserialize(JsonParser parser, DeserializationContext context) throws IOException, JacksonException {
    JsonNode node = parser.getCodec().readTree(parser);
    String name = node.get("name").asText();
    return MAP.computeIfAbsent(name, nameKey -> new Column(nameKey, node.get("type").asText()));
  }

  public static Map<String, Column> getMap() {
    return Collections.unmodifiableMap(MAP);
  }
}

We need the static instance of the map in order to share it with KeyDeserializer, getMap() returns unmodifiableMap so we can't change it by mistake. Then your KeyDeserializer will use that map to get existing instances.

public class CachedColumnKeyDeserializer extends KeyDeserializer {

  private final Map<String, Column> map;

  public CachedColumnKeyDeserializer() {
    this.map = CachingColumnDeserializer.getMap();
  }

  @Override
  public Object deserializeKey(String key, DeserializationContext context) throws IOException {
    Column column = this.map.get(key);
    if (column == null) {
      return new Column(key, null);
    }
    return column;
  }
}

Specify how to deserialize Column class

@JsonDeserialize(using = CachingColumnDeserializer.class, keyUsing = CachedColumnKeyDeserializer.class)

Just to be on the safe side you can specify you need to deserialize columns before other properties

@JsonPropertyOrder({"name", "columns", ...})

Upvotes: 1

Related Questions