Search code examples
javajacksonjackson-databind

Jackson gives StackOverflowError reading value in custom StdDeserializer


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.


Solution

  • 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);