web-dev-qa-db-fra.com

Désérialisation de Jackson en fonction du type

Disons que j'ai JSON du format suivant:

{
    "type" : "Foo"
    "data" : {
        "object" : {
            "id" : "1"
            "fizz" : "bizz"
            ...
        },
        "metadata" : {
            ...
        },
        "owner" : {
            "name" : "John"
            ...
        }
    }
}

J'essaie d'éviter le désérialiseur personnalisé et de désérialiser le code JSON ci-dessus (appelé Wrapper.Java) en fichiers POJO Java. Le champ "type" dicte la désérialisation "objet" c'est-à-dire. type = foo signifie que le champ "objet" est désérialisé à l’aide de Foo.Java. (si type = Bar, utilisez Bar.Java pour désérialiser le champ de l'objet). Les métadonnées/propriétaires vont toujours désérialiser de la même manière en utilisant une simple classe Java annotée de Jackson pour chacune. Y a-t-il un moyen d'accomplir cela en utilisant des annotations? Sinon, comment cela peut-il être fait avec un désérialiseur personnalisé?

7
John Baum

Approche d'annotations uniquement

À la place de l’approche custom deserializer , vous pouvez utiliser les éléments suivants pour une solution ne contenant que des annotations (similaire à celle décrite dans la réponse de Spunc , mais en utilisant type comme propriété external ):

public abstract class AbstractData {

    private Owner owner;

    private Metadata metadata;

    // Getters and setters
}
public static final class FooData extends AbstractData {

    private Foo object;

    // Getters and setters
}
public static final class BarData extends AbstractData {

    private Bar object;

    // Getters and setters
}
public class Wrapper {

    private String type;

    @JsonTypeInfo(use = Id.NAME, property = "type", include = As.EXTERNAL_PROPERTY)
    @JsonSubTypes(value = { 
            @JsonSubTypes.Type(value = FooData.class, name = "Foo"),
            @JsonSubTypes.Type(value = BarData.class, name = "Bar") 
    })
    private AbstractData data;

    // Getters and setters
}

Dans cette approche, @JsonTypeInfo est configuré pour utiliser type en tant que propriété external afin de déterminer la bonne classe pour mapper la propriété data.

Le document JSON peut être désérialisé comme suit:

ObjectMapper mapper = new ObjectMapper();
Wrapper wrapper = mapper.readValue(json, Wrapper.class);  
10
cassiomolin

Approche désérialiseur personnalisé

Vous pouvez utiliser un désérialiseur personnalisé qui contrôle la propriété type pour analyser la propriété object dans la classe la plus appropriée.

Commencez par définir une interface qui sera implémentée par les classes Foo et Bar:

public interface Model {

}
public class Foo implements Model {

    // Fields, getters and setters
}
public class Bar implements Model {

    // Fields, getters and setters
}

Puis définissez vos classes Wrapper et Data:

public class Wrapper {

    private String type;

    private Data data;

    // Getters and setters
}
public class Data {

    @JsonDeserialize(using = ModelDeserializer.class)
    private Model object;

    private Metadata metadata;

    private Owner owner;

    // Getters and setters
}

Le champ object est annoté avec @JsonDeserialize , indiquant le désérialiseur qui sera utilisé pour la propriété object.

Le désérialiseur est défini comme suit:

public class ModelDeserializer extends JsonDeserializer<Model> {

    @Override
    public Model deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException, JsonMappingException {

        // Get reference to ObjectCodec
        ObjectCodec codec = jp.getCodec();

        // Parse "object" node into Jackson's tree model
        JsonNode node = codec.readTree(jp);

        // Get value of the "type" property
        String type = ((Wrapper) jp.getParsingContext().getParent()
            .getCurrentValue()).getType();

        // Check the "type" property and map "object" to the suitable class
        switch (type) {

            case "Foo":
                return codec.treeToValue(node, Foo.class);

            case "Bar":
                return codec.treeToValue(node, Bar.class);

            default:
                throw new JsonMappingException(jp, 
                    "Invalid value for the \"type\" property");
        }
    }
}

Le document JSON peut être désérialisé comme suit:

ObjectMapper mapper = new ObjectMapper();
Wrapper wrapper = mapper.readValue(json, Wrapper.class);  

En guise d'alternative à ce désérialiseur personnalisé, envisagez une approche annotations uniquement .

7
cassiomolin

Tout cela peut être fait au moyen d'annotations.

Créez une super-classe abstraite avec les champs communs tels que "métadonnées" et "propriétaire" et leurs getters/setters. Cette classe doit être annotée avec @JsonTypeInfo . Cela devrait ressembler à:

@JsonTypeInfo(use = Id.CLASS, include = As.PROPERTY, property = "type")

Avec le paramètre property = "type", vous indiquez que l'identificateur de classe sera sérialisé dans le champ type de votre document JSON.

La valeur de l'identificateur de classe peut être spécifiée avec use. Id.CLASS utilise le nom de classe Java qualifié complet. Vous pouvez également utiliser Id.MINIMAL_CLASS, qui est un nom de classe Java abrégé. Pour avoir votre propre identifiant, utilisez Id.NAME. Dans ce cas, vous devez déclarer les sous-types:

@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Foo.class, name = "Foo"),
    @JsonSubTypes.Type(value = Bar.class, name = "Bar")
})

Implémentez vos classes Foo and Bar en partant de la superclasse abstraite.

ObjectMapper de Jackson utilisera le champ supplémentaire "type" du document JSON pour la sérialisation et la désérialisation. Par exemple. Lorsque vous désérialiserez une chaîne JSON en une référence de super classe, elle appartiendra à la sous-classe appropriée:

ObjectMapper om = new ObjectMapper();
AbstractBase x = om.readValue(json, AbstractBase.class);
// x will be instanceof Foo or Bar


Exemple de code complet (j’ai utilisé des champs publics comme raccourci pour ne pas avoir besoin d’écrire des getters/setters):

package test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;

import Java.io.IOException;

import com.fasterxml.jackson.annotation.JsonSubTypes;

@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Foo.class, name = "Foo"),
    @JsonSubTypes.Type(value = Bar.class, name = "Bar")
})
public abstract class AbstractBase {

    public MetaData metaData;
    public Owner owner;
    @Override
    public String toString() {
        return "metaData=" + metaData + "; owner=" + owner;
    }

    public static void main(String[] args) throws IOException {

        // Common fields
        Owner owner = new Owner();
        owner.name = "Richard";
        MetaData metaData = new MetaData();
        metaData.data = "Some data";

        // Foo
        Foo foo = new Foo();
        foo.owner = owner;
        foo.metaData = metaData;
        CustomObject customObject = new CustomObject();
        customObject.id = 20l;
        customObject.fizz = "Example";
        Data data = new Data();
        data.object = customObject;
        foo.data = data;
        System.out.println("Foo: " + foo);

        // Bar
        Bar bar = new Bar();
        bar.owner = owner;
        bar.metaData = metaData;
        bar.data = "A String in Bar";

        ObjectMapper om = new ObjectMapper();

        // Test Foo:
        String foojson = om.writeValueAsString(foo);
        System.out.println(foojson);
        AbstractBase fooDeserialised = om.readValue(foojson, AbstractBase.class);
        System.out.println(fooDeserialised);

        // Test Bar:
        String barjson = om.writeValueAsString(bar);
        System.out.println(barjson);
        AbstractBase barDeserialised = om.readValue(barjson, AbstractBase.class);
        System.out.println(barDeserialised);

    }

}

class Foo extends AbstractBase {
    public Data data;
    @Override
    public String toString() {
        return "Foo[" + super.toString() + "; data=" + data + ']';
    }
}

class Bar extends AbstractBase {
    public String data;
    public String toString() {
        return "Bar[" + super.toString() + "; data=" + data + ']';
    }
}


class Data {
    public CustomObject object;
    @Override
    public String toString() {
        return "Data[object=" + object + ']';
    }
}

class CustomObject {
    public long id;
    public String fizz;
    @Override
    public String toString() {
        return "CustomObject[id=" + id + "; fizz=" + fizz + ']';
    }
}

class MetaData {
    public String data;
    @Override
    public String toString() {
        return "MetaData[data=" + data + ']';
    }
}

class Owner {
    public String name;
    @Override
    public String toString() {
        return "Owner[name=" + name + ']';
    }
}
6
Spunc

Je pense que c'est plutôt simple. Vous avez probablement une super classe qui a des propriétés pour metadata et owner. Ainsi, plutôt que de le rendre vraiment générique, vous pouvez remplacer T par votre super classe. Mais fondamentalement, vous devrez analyser le nom de la classe à partir de la chaîne JSON réelle, qui dans votre exemple ressemblerait à ceci:

int start = jsonString.indexOf("type");
int end = jsonString.indexOf("data");
Class actualClass = Class.forName(jsonString.substring(start + 4, end - 2)); // that of course, is approximate - based on how you format JSON

et le code général pourrait être quelque chose comme ceci:

public static <T> T deserialize(String xml, Object obj)
        throws JAXBException {

    T result = null;

    try {

        int start = jsonString.indexOf("type");
        int end = jsonString.indexOf("data");
        Class actualClass = Class.forName(jsonString.substring(start + 4, end - 2)); 

        JAXBContextFactory factory = JAXBContextFactory.getInstance();
        JAXBContext jaxbContext = factory.getJaxBContext(actualClass);

        Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();

        // this will create Java object
        try (StringReader reader = new StringReader(xml)) {
            result = (T) jaxbUnmarshaller.unmarshal(reader);
        }

    } catch (JAXBException e) {
        log.error(String
                .format("Exception while deserialising the object[JAXBException] %s\n\r%s",
                        e.getMessage()));
    }

    return result;
}
1
Renats Stozkovs