Is it possible to set Jackson in order to exclude unnamed wrapper objects into a JSON array returned by an API?

Viewed 104

I am working on a Spring Boot application and I am finding some problem converting a JSON object (retrieved by a REST call performed using RestTemplate) into a domain object.

This is my main domain object:

public class NotaryDistrict {
    String idDistrict;
    String denominazione;
    String regione;
    String provincia;
    ArrayList<Localita> localita;

    String distretto;
    String indirizzo;
    String cap;
    String telefono;
    String fax;
    String email;
    String pec;
    String webUrl;

    ArrayList<Carica> cariche;


    public NotaryDistrict() {
        super();
    }


    public NotaryDistrict(String idDistrict, String denominazione, String regione, String provincia,
            ArrayList<Localita> localita, String distretto, String indirizzo, String cap, String telefono, String fax,
            String email, String pec, String webUrl, ArrayList<Carica> cariche) {
        super();
        this.idDistrict = idDistrict;
        this.denominazione = denominazione;
        this.regione = regione;
        this.provincia = provincia;
        this.localita = localita;
        this.distretto = distretto;
        this.indirizzo = indirizzo;
        this.cap = cap;
        this.telefono = telefono;
        this.fax = fax;
        this.email = email;
        this.pec = pec;
        this.webUrl = webUrl;
        this.cariche = cariche;
    }


    public String getIdDistrict() {
        return idDistrict;
    }


    public void setIdDistrict(String idDistrict) {
        this.idDistrict = idDistrict;
    }


    public String getDenominazione() {
        return denominazione;
    }


    public void setDenominazione(String denominazione) {
        this.denominazione = denominazione;
    }


    public String getRegione() {
        return regione;
    }


    public void setRegione(String regione) {
        this.regione = regione;
    }


    public String getProvincia() {
        return provincia;
    }


    public void setProvincia(String provincia) {
        this.provincia = provincia;
    }


    public ArrayList<Localita> getLocalita() {
        return localita;
    }


    public void setLocalita(ArrayList<Localita> localita) {
        this.localita = localita;
    }


    public String getDistretto() {
        return distretto;
    }


    public void setDistretto(String distretto) {
        this.distretto = distretto;
    }


    public String getIndirizzo() {
        return indirizzo;
    }


    public void setIndirizzo(String indirizzo) {
        this.indirizzo = indirizzo;
    }


    public String getCap() {
        return cap;
    }


    public void setCap(String cap) {
        this.cap = cap;
    }


    public String getTelefono() {
        return telefono;
    }


    public void setTelefono(String telefono) {
        this.telefono = telefono;
    }


    public String getFax() {
        return fax;
    }


    public void setFax(String fax) {
        this.fax = fax;
    }


    public String getEmail() {
        return email;
    }


    public void setEmail(String email) {
        this.email = email;
    }


    public String getPec() {
        return pec;
    }


    public void setPec(String pec) {
        this.pec = pec;
    }


    public String getWebUrl() {
        return webUrl;
    }


    public void setWebUrl(String webUrl) {
        this.webUrl = webUrl;
    }



    public ArrayList<Carica> getCariche() {
        return cariche;
    }


    public void setCariche(ArrayList<Carica> cariche) {
        this.cariche = cariche;
    }


    @Override
    public String toString() {
        return "NotaryDistrict [idDistrict=" + idDistrict + ", denominazione=" + denominazione + ", regione=" + regione
                + ", provincia=" + provincia + ", localita=" + localita + ", distretto=" + distretto + ", indirizzo="
                + indirizzo + ", cap=" + cap + ", telefono=" + telefono + ", fax=" + fax + ", email=" + email + ", pec="
                + pec + ", webUrl=" + webUrl + ", cariche=" + cariche + "]";
    }
}

As you can see it contains this array field:

ArrayList<Carica> cariche;

This is the field that is giving me problem (if I exclude this one commenting it, it works fine...the others field are correctly mapped)

This is the Carica domain object:

public class Carica {
    
    String idNotary;
    String nome;
    String cognome;
    String carica;
    
    public Carica() {
        super();
        // TODO Auto-generated constructor stub
    }

    public Carica(String idNotary, String nome, String cognome, String carica) {
        super();
        this.idNotary = idNotary;
        this.nome = nome;
        this.cognome = cognome;
        this.carica = carica;
    }

    public String getIdNotary() {
        return idNotary;
    }

    public void setIdNotary(String idNotary) {
        this.idNotary = idNotary;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getCognome() {
        return cognome;
    }

    public void setCognome(String cognome) {
        this.cognome = cognome;
    }

    public String getCarica() {
        return carica;
    }

    public void setCarica(String carica) {
        this.carica = carica;
    }

    @Override
    public String toString() {
        return "NotaryPosition [idNotary=" + idNotary + ", nome=" + nome + ", cognome=" + cognome + ", carica=" + carica
                + "]";
    }

}

Into my business logic code I perform the API call in this way:

ResponseEntity forEntity2 = restTemplate.getForEntity(uri, NotaryDistrict.class); NotaryDistrict notaryDistrictDetails = forEntity2.getBody();

System.out.println("notaryDistric details: " + notaryDistrictDetails);

{
    "idDistrict": "CG7drXn9fvA%253D",
    "distretto": "SCIACCA",
    "denominazione": "Agrigento e Sciacca",
    "provincia": "Agrigento",
    "regione": "Sicilia",
    "indirizzo": "Viale della Vittoria n.319",
    "cap": "92100",
    "telefono": "092220111",
    "fax": "09222111",
    "email": "xxx@yyy.it",
    "pec": "zzzz@postacertificata.yyy.it",
    "webUrl": null,

    "cariche": [
        {
            "carica": {
                "idNotary": "e12oYuuTvE4%253D",
                "nome": "Claudia",
                "cognome": "Rossi",
                "carica": "Presidente"
            }
        },
        {
            "carica": {
                "idNotary": "XlB2DSwWbfE%253D",
                "nome": "Maria",
                "cognome": "Verdi",
                "carica": "Segretario"
            }
        },
        {
            "carica": {
                "idNotary": "W8I4vogJ0OM%253D",
                "nome": "Giuseppe",
                "cognome": "Bianchi",
                "carica": "Tesoriere"
            }
        },
        {
            "carica": {
                "idNotary": "DR6Y%252BA37%252Few%253D",
                "nome": "ARIANNA",
                "cognome": "Ciani",
                "carica": "Consigliere"
            }
        },
    ]
}

So all the fields excepet the cariche array are correctly mapped in my NotaryDistrict main domain objects.

The problem occurs when I add the ArrayList cariche; field to this domain object.

I expected that every object into the cariche JSON array has to be mapped with an object into the cariche array of my class.

But I am obtaining this exception:

Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.String` from Object value (token `JsonToken.START_OBJECT`)
 at [Source: (PushbackInputStream); line: 1, column: 363] (through reference chain: com.notariato.updateInfo.domain.NotaryDistrict["cariche"]->java.util.ArrayList[0]->com.notariato.updateInfo.domain.Carica["carica"])
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.12.4.jar:2.12.4]

The reason of this exception is pretty clear to me: the proble is that the JSON array named cariche contains a wrapper object {...} that itself contains a carica object.

I think that a possible solution is to create a second level wrapper domain object but it is pretty ugly.

Exist a way to set Jackson in order to ignore this {...} wrapper objects and consider only its content, the carica object that must be mapped into this Java array:

ArrayList<Carica> cariche;
1 Answers

One way to do this is to write a custom deserializer. (In fact, if Ralph's comment is correct, this is currently the only way.) You do need to add one more class to your application, but it's a short class.

Below, you'll find a Spring Boot test which includes such a deserializer. As the comments say, it's not production-ready code, but the tests pass, and modifying it to do what you want should be reasonably easy. You may want to read up on how to write Jackson deserializers; if you find any good resource on that, please post it in a comment, since as far as I can tell none exist. Thanks are due to Eugen Paraschiv, one of whose characteristically laconic articles I referenced.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

// non-static imports omitted for brevity

@RestClientTest
class SampleApplicationTests {
    @Autowired MockRestServiceServer server;
    @Autowired RestTemplate template;

    @TestConfiguration static class Config {
        @Bean RestTemplate template(@Autowired RestTemplateBuilder builder) { return builder.build(); }
    }

    @Data @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor static class ElementEntity {
        String key;
    }

    @Data static class NiceEntityWithList { List<ElementEntity> list; }

    @SuppressWarnings("serial") static class WeirdEntityDeserializer extends StdDeserializer<ElementEntity> {
        protected WeirdEntityDeserializer() { this(null); }
        protected WeirdEntityDeserializer(Class<?> vc) { super(vc); }

        @Override public ElementEntity deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            ObjectCodec codec = p.getCodec();
            JsonNode weirdElementNode = codec.readTree(p);
            JsonNode realElementNode = weirdElementNode.get("common");
            // in a real app, handle the case where realElementNode turns out to be null

            // we punt to a Jackson method here so this deserializer uses as much of the rest of
            // your Jackson configuration as possible
            return codec.treeToValue(realElementNode, ElementEntity.class);
            // in a real app, handle the case where realElementNode.get("key") is null
            // (you may also want to do some sort of validation here)
        }
    }

    @Data static class WeirdEntityWithList {
        @JsonDeserialize(contentUsing = WeirdEntityDeserializer.class) List<ElementEntity> list;
    }

    @Test void niceListDeserializes() {
        final String niceJson = "{\"list\": [{\"key\": \"value\"}, {\"key\": \"value2\"}]}";
        this.server.expect(requestTo("/nicelist")).andRespond(withSuccess(niceJson, MediaType.APPLICATION_JSON));
        NiceEntityWithList nice = template.getForEntity("/nicelist", NiceEntityWithList.class).getBody();

        assertEquals(new ElementEntity("value"), nice.getList().get(0));
        // could just use nice.list, but let's pretend we're writing
        // code for real and the class is in another package
        assertEquals(new ElementEntity("value2"), nice.getList().get(1));
    }

    @Test void weirdListDeserializes() {
        final String weirdJson = "{\"list\": [{\"common\": {\"key\": \"value\"}}, {\"common\": {\"key\": \"value2\"}}]}";
        server.expect(requestTo("/weirdlist")).andRespond(withSuccess(weirdJson, MediaType.APPLICATION_JSON));
        WeirdEntityWithList weird = template.getForEntity("/weirdlist", WeirdEntityWithList.class).getBody();

        assertEquals(new ElementEntity("value"), weird.getList().get(0));
        assertEquals(new ElementEntity("value2"), weird.getList().get(1));
    }
}

This should work in a project downloaded from start.spring.io; you'll need to add spring-starter-web and lombok as dependencies, and put spring.main.web-application-type: none into the application.properties.

Note: If I remove the @JsonDeserialize from the WeirdEntityWithList definition above, the tests do fail, but they don't fail like your application fails -- there's no exception; the fields of the list's elements just get set to null. I suspect this is related to some difference between Spring Boot's default Jackson configuration and yours. (You may also be using a different Spring Boot version, etc.) I hope whatever's causing the difference doesn't make the code useless to you.

Related