Having 2 simple classes like:
@Setter
@Getter
public class Continent {
private String id;
private String code;
private String name;
}
@Setter
@Getter
public class Country {
private String id;
private String alpha2Code;
private String alpha3Code;
private String name;
private Continent continent;
}
when reading the following yaml:
id: brazil
alpha2_code: BR
alpha3_code: BRA
name: Brazil
continent_id: south-america
I would like to use the continent_id
to retrieve the Continent
from a application scoped List<Continent>
.
The best thing I could think of is using a custom Deserializer like:
public class CountryDeserializer extends StdDeserializer<Country> {
public CountryDeserializer() {
super(Country.class);
}
@Override
public Country deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// This works... the `continentId` is retrieved!
JsonNode node = jp.getCodec().readTree(jp);
String continentId = node.get("continent_id").asText();
// How to access application scoped continents? Use injectable value?
Continent continent = getContinent(continentId);
// Read value for other properties; don't want to read other properties manually!
Country country = jp.getCodec().readValue(jp, Country.class);
// But unfortunately this throws a StackOverflow...
country.setContinent(continent);
return country;
}
}
But the problem is I would like Jackson to automatically read the other properties. I don't want to this manually as if in the future a property is added it might be forgotten, and with other entities with 20 properties this becomes very cumbersome...
I tried with Country country = jp.getCodec().readValue(jp, Country.class);
but that gives stack overflow exception as it gets in a loop with the custom deserializer obviously.
Is there a way to solve this using Jackson, or is there another better approach to get and set the Continent
in this scenario?
Note I'm working with a pre-defined set of domain classes I cannot change. I can modify the object mapper and add mixins if needed.
Instead of using a CountryDeserializer
I've implemented it using a ContinentReferenceDeserializer
.
This way the other Country
properties are deserialized "automatically".
It looks like:
public class ContinentReferenceDeserializer extends StdDeserializer<Continent> {
public ContinentReferenceDeserializer() {
super(Continent.class);
}
@Override
public Continent deserialize(JsonParser parser, DeserializationContext context) throws IOException {
String id = parser.getText(); // returns the continent id (`continent_id` in json)
Map<String, Continent> continents = (Map<String, Continent>) context.findInjectableValue("continents", null, null);
return continents.gett(id);
}
}
and it is used in the CountryMixIn
like:
public abstract class CountryMixIn {
@JsonProperty("continent_id")
@JsonDeserialize(using = ContinentReferenceDeserializer.class)
abstract Continent getContinent();
}
Note that if you don't use Mix-ins but directly annotate domain/dtoa classes, above can be applied to these as well instead.
The ObjectMapper
can be setup then like:
Map<String, Continent> continents = .. // get the continents
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Country.class, CountryMixIn.class);
mapper.setInjectableValues(new InjectableValues.Std().addValue("continents", continents));
and then can be called like:
String json = .. // get the json
Country country = mapper.readValue(json, Country.class);