0
votes

I am glued with some Jackson polymorphic problem.

I work on a web JDR Character Editor personnal project. I use Springboot and try to stuck with the phylosophy. Moreover, I try to make some independent packages, because of study-case for my real work (another springboot project).

With no Jackson configuration, I have no problem for serialization of a Competence. But when I try to get back any modification on the web editor, so when Jackson make a deserialization of a Competence, problems occur with "dependance" property.

Here are my classes:

The one I try to serialize/deserialize:

public class Competence implements Composante, ComposanteTemplate {

    public enum Categorie {
        APPRENTI,
        COMPAGNON
    }

    private String nom;
    private String description;
    private Categorie categorie;
    private Chapitre chapitre;
    private AttributTemplate dependance;
    private List sousCompetences = new ArrayList();

    public String getNom() {
        return nom;
    }

    public void setNom(String nom) {
        this.nom = nom;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Competence getTemplate() {
        return this;
    }

    public Categorie getCategorie() {
        return categorie;
    }

    public void setCategorie(Categorie categorie) {
        this.categorie = categorie;
    }

    public Chapitre getChapitre() {
        return chapitre;
    }

    public void setChapitre(Chapitre chapitre) {
        this.chapitre = chapitre;
    }

    public AttributTemplate getDependance() {
        return dependance;
    }

    public void setDependance(AttributTemplate dependance) {
        this.dependance = dependance;
    }

    public List getSousCompetences() {
        return sousCompetences;
    }

    public void setSousCompetences(List sousCompetences) {
        this.sousCompetences = sousCompetences;
    }

    public boolean isOuverte() {
        return !sousCompetences.isEmpty();
    }
}

The superclass of the property I have a problem with:

public interface AttributTemplate extends ComposanteTemplate {}

The two subclasses which could be use for Competence#dependance property:

public enum Carac implements AttributTemplate, Attribut {

    FORT(Type.PHYSIQUE),
    AGILE(Type.PHYSIQUE),
    RESISTANT(Type.PHYSIQUE),
    OBSERVATEUR(Type.PHYSIQUE),
    SAVANT(Type.MENTALE),
    RUSE(Type.MENTALE),
    TALENTUEUX(Type.MENTALE),
    CHARMEUR(Type.MENTALE);

    public enum Type {
        PHYSIQUE,
        MENTALE
    }

    public final Type type;
    public final String nom = name().toLowerCase();

    private String description;

    Carac(Type type) {
        this.type = type;
    }

    @Override
    public String getNom() { return nom; }

    @Override
    public String getDescription() { return description;  }

    @Override
    public Carac getTemplate() { return this; }

    public void setDescription(String description) { this.description = description; }

}
public enum ArtTemplate implements AttributTemplate {

    ART_GUERRIER(2, 1),
    ART_ETRANGE(1, 2),
    ART_GUILDIEN(1, 1);

    public static final String ART_PREFIX = "ART";

    public final String nom = name().toLowerCase().replace("_", " ");
    public final int nbCaracsPhysiques;
    public final int nbCaracsMentales;

    private String description;

    ArtTemplate(int nbCaracsPhysiques, int nbCaracsMentales) {
        this.nbCaracsMentales = nbCaracsMentales;
        this.nbCaracsPhysiques = nbCaracsPhysiques;
    }

    @Override
    public String getNom() {
        return nom;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getNbCaracs() {
        return nbCaracsPhysiques + nbCaracsMentales;
    }

}

The result json (and then the json I send) is:

{"nom":"Comp_1489746646510","description":"ezbuixnwrclfvmgwdviubcauenzytpzzvumnohwyhpuynxaqhkjdbqygtrmbtlschthovuyoiolkauucwokkfjnaujnufshrjboykuqce","categorie":"APPRENTI","chapitre":"GUERRE","dependance":"ART_ETRANGE","ouverte":false,"sousCompetences":[]}

QUESTION: I understand that my problem is caused by the abstract relation AttributTemplate, and then when Jackson try to deserialize, he does not know which of Carac or ArtTemplate class to use. I try to keep unchanged Competence (Competence come from an external jar), so no annotation on this class is possible.

I've tried many of the solutions I found (Jackson 1.5: Polymorphic Type Handling, first steps ) and the only one which has worked was to define a DeserializationProblemHandler

mapper.addHandler(new DeserializationProblemHandler() {
 @Override
 public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, JsonParser p, String msg) throws IOException {
     if (instClass == AttributTemplate.class) {
         String name = p.getText();
         return !name.startsWith(ArtTemplate.ART_PREFIX) ? Carac.valueOf(name) : ArtTemplate.valueOf(name);
     }
     return super.handleMissingInstantiator(ctxt, instClass, p, msg);
 }

});

But I feel bad with this solution, because I am sure there is an other beautiful one.

So is it possible to configure the mapper in order that he is able to determine which of Carac or ArtTemplate he must use to get AttributTemplate?


EDIT: I managed to have this:

{"nom":"Comp_1489756873433","description":"kruzueemlwisibshlkotasayfkhdqkqolvhlqgsnntndkpvbmmgklqysabiakaolempmupeyiqaztdcrhwimdksgzybbdzttwnwqjxhfo","categorie":"COMPAGNON","chapitre":"GUERRE","dependance":["mova.ged.perso.inne.Carac","AGILE"],"ouverte":true,"sousCompetences":[...]}

by configuring like this the mapper

    abstract class CompetenceMixIn {

        private AttributTemplate dependance;

        @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.EXISTING_PROPERTY, property="dependance")
        @JsonSubTypes({ @JsonSubTypes.Type(value = Carac.class, name = "carac"), @JsonSubTypes.Type(value = ArtTemplate.class, name = "artTemplate") })
        public void setDependance(AttributTemplate dependance) {
            this.dependance = dependance;
        }
    }
ObjectMapper mapper = jsonConverter.getObjectMapper();
mapper.addMixIn(Competence.class, CompetenceMixIn.class);

As you could see, I'm still parasited with the array that wrapped dependance value. I would (...)"dependance": "AGILE", (...) not (...)"dependance":["mova.ged.perso.inne.Carac", "AGILE"], (...)
And I don't know what to change in order to have this.

1
I have answered a similar question before. Jackson allows for this to be configurable. I don't want to reinvent this for your specific case, so please have a look here: stackoverflow.com/questions/38501574/… The example there is very simple. The Params class has 2 implementations and I tell jackson what property to use to lookup how to deserialize itpandaadb
I try your example, but still can't find the real solution. I modified my post with the begining of solution I found thanks to you.Mohicane
Still get the problem .... I can't imagine having such problems for something which seems to me so simple??Mohicane

1 Answers

1
votes

i have been looking into what you are trying to do. Unfortunatelly, I believe there are issues with Enums + inheritance.

I have an alternative solution that you could be using which is to use a custom creator and ignore unknown properties. See the following example:

public class JacksonInheritance {

    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();

        Competence c = new Competence();
        c.desc = "desc";
        c.nome = "nome";
        c.template = Att1.TEST_Att1;
        String test = mapper.writeValueAsString(c);
        System.out.println(test);

        Competence readValue = mapper.readValue(test, Competence.class);
        System.out.println(readValue.template);
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Competence {

        private static final Map<String, AttributeTemplate> templates;
        static {
            templates = new HashMap<>();
            Stream.of(Att1.values()).forEach( a -> templates.put(a.name(), a));
            Stream.of(Att2.values()).forEach( a -> templates.put(a.name(), a));
        }

        @JsonProperty
        String nome;
        @JsonProperty
        String desc;
        @JsonIgnore
        AttributeTemplate template;

        @JsonProperty("template_type")
        public String getTempl() {
            // Here you can do whichever way uou would like to serialise your template. This will be the key
            return template.toString();
        }

        @JsonCreator
        public static Competence create(@JsonProperty("template_type") String templateType) {
            Competence c = new Competence();
            c.template =  templates.get(templateType);
            return c;
        }
    }

    public static interface AttributeTemplate {
    }

    public static enum Att1 implements AttributeTemplate {
        TEST_Att1;
    }

    public static enum Att2 implements AttributeTemplate {

        TEST2_Att2;
    }
}

Here I am detaching the enum logic from the jackson logic and implement my own. This does not require a custom serialisation.

I basically say that I serialise my enum as its value (you can obviously choose which ever properties you would like for this).

My output json then looks as:

{"template_type":"TEST_Att1","nome":"nome","desc":"desc"}

At the return step I now know that the information I need to construct the correct enum template type from the template_type attribute. This is what I can inject into my factory method create.

In the create I can use my statically created map to populate the correct enum into my object. We can just create this map statically since our enums are finite and static.

The beauty of this is also that the generator is only used for creation. Using @JsonIgnoreProperties(ignoreUnknown = true), we can tell jackson to not freak out by all our custom elements in the json. It will simply deserialise any fields it can detect and leave the other ones (since we are using a custom template_type for our enum resolution).

Finally, I am ignoring the actual template in my bean because jackson won't be able to construct that.

I hope that this works for you/helps you. Sorry about the delay.

Reason for not using inheritance:

  1. There seem to be issues with enum + inheritance in jackson. Particularly jackson by default uses reflection and calls the private constructor of the enum for generation. You may be able to get creators to work in a similar way as above though.

  2. The deserialisation expects the template. I am going of the assumption that you do NOT necessarily want to serialise all elements of the enum. This is because the enum name, in my case TEST_Att1 makes the enum unique. There is no need to serialise and send all the different attributes these enums have around. However, Deserialization with @JsonSubTypes for no value - missing property error shows that jackson requires your template field to be at least present. This is a a slight issue, because you want to use an external property for this instead (so why include a null-field as suggested in your json just to make jackson happy)

This may not be the best solution, but I think it is relatively elegant given the restrictions. I hope that helps you,

Artur