Search code examples
javajsonserializationlibgdx

Deserialising set of enums to use as flags


I want to be able to set enum/bit flags from Json. I have managed to serialize my object with a HashSet containing my enum values. By default LibGDX serializes this but adds a class field with the type of set to the json so it knows what to do. I want my json to be clean and decoupled from java so I wrote this class:

public class CriteriaSerializer implements Json.Serializer<HashSet> {

    @Override
    public void write(Json json, HashSet object, Class knownType) {
        json.writeArrayStart();
        for (Object o : object)
        {
            if (o instanceof Modifier.Criteria) {
                json.writeValue(o, Modifier.Criteria.class);
            }
        }
        json.writeArrayEnd();
    }

    @Override
    public HashSet read(Json json, JsonValue jsonData, Class type) {
        System.out.println("Running!?");
        HashSet<Modifier.Criteria> criteriaSet = new HashSet<Modifier.Criteria>();

        for (JsonValue entry = jsonData.child; entry != null; entry = entry.next)
        {
            criteriaSet.add(Modifier.Criteria.valueOf("ADD"));//Modifier.Criteria.valueOf(entry.asString()));
        }

        return criteriaSet;
    }
}

The write method results in the following output:

modifier: {
    amount: 1 //Other field
    criteriaSet: [
        RED
        BLUE
    ]

All I need is to get those values as strings so I can do something along the lines of myCriteriaSet.put(Criteria.valueOf(output). The thing is, the program crashes before the read method is running. I guess this is because it finds an ArrayList in the json data but the corresponding field in the object is a HashSet. This is the error java.lang.IllegalArgumentException: Can not set java.util.Set field com.buckriderstudio.towercrawler.Creature.Modifier.criteriaSet to java.util.ArrayList

Both writing to- and reading from json is important to me so I need them to work with eachother. In the end I'm just looking for a clean solution to get (de)serialize EnumSet or bit combinations in a readable manner. I feel I'm close but there might be a better technique then the one I'm trying.

What I like about the LibgDX Json implementation is that fields are not mandatory and can have default values. This cleans up the json data considerably since I have a lot of fields that can be set optionally. Therefor this library has my preference to say Jackson, but I have not played around with Jackson all that much though.

EDIT

This is an edit especially for Andreas. As far as I know (but I might be wrong) this has nothing to do with the actual problem. Andreas is explaining to me that the Json syntax is wrong, the fact is that it does not even reach my read method and that the json library that ships with LibGDX is not writing 100% correct Json. Does it need to? Perhaps to bear the name Json? Does it need to to work? I don't think so.

Here is my test. All I do is create this Creature object and parse it with 1 line of code. There is no personal code of me involved in parsing this.

Creature c = new Creature("MadMenyo");
System.out.println(json.prettyPrint(c));

//Output

{
name: MadMenyo
modifier: {
    amount: 1
    criteriaSet: {
        class: java.util.HashSet
        items: [
            VS_NATURE
            MULTIPLY
        ]
    }
}
stats: {
    ENDURANCE: {
        abbreviation: END
        displayName: Endurance
        baseValue: 8
        finalValue: 8
    }
    MAGIC: {
        abbreviation: MP
        displayName: Your mana
        baseValue: 20
        finalValue: 20
    }
    STRENGTH: {
        baseValue: 6
        finalValue: 6
    }
    HEALTH: {
        abbreviation: HP
        displayName: Your life
        baseValue: 100
        finalValue: 100
    }
}
}

//Looks like no valid Json to me. But the following line parses that correctly into a Creature object.

Creature jsonCreature = json.fromJson(Creature.class, jsonCreature);

Before we drift off even further. The reason why I do not want to use this is because it outputs the class class: java.util.HashSet and I'm pretty sure that is unnecessary.

EDIT

After adding the following lines of code I managed to output correct json. Yet the code still breaks before it gets to my custom read method. The question remain how to either fix that or serialize a Enumset or other Set holding enums in a different way as long as it is readable in Json and can be used as flags.

    JsonWriter jw = new JsonWriter(new StringWriter());
    json.setOutputType(JsonWriter.OutputType.json);
    json.setWriter(jw);

//Now outputs proper Json

{
"name": "MadMenyo",
"modifier": {
    "amount": 1,
    "criteriaSet": [
        "VS_NATURE",
        "MULTIPLY"
    ]
},
"stats": {
    "ENDURANCE": {
        "abbreviation": "END",
        "displayName": "Endurance",
        "baseValue": 8,
        "finalValue": 8
    },
    "MAGIC": {
        "abbreviation": "MP",
        "displayName": "Your mana",
        "baseValue": 20,
        "finalValue": 20
    },
    "STRENGTH": {
        "baseValue": 6,
        "finalValue": 6
    },
    "HEALTH": {
        "abbreviation": "HP",
        "displayName": "Your life",
        "baseValue": 100,
        "finalValue": 100
    }
}

Solution

  • Although this isn't exactly answering my question, since it's still crashing before it gets to the read method, I have found a suitable work around using the Jackson library. I have figured out how to disregard default values by using the following annotation on the class to be serialized: @JsonInclude(JsonInclude.Include.NON_DEFAULT). This gets me the exact json output as what I was attempting with the build in json serializer. The only downside is the speed, Jackson is about 20 times slower on a single Object but looping that a 1000 times makes it "only" about 5 times slower.

    For anyone who does not know, this is how you integrate Jackson with LibGDX:

    In build add a dependency to the core project.

    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.0.1'
    

    Starting parsing takes just a couple more lines.

        ObjectMapper mapper = new ObjectMapper();
        //Add pretty print indentation
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
    
        //When serializing we have to wrap it in a try/catch signature
        try {
            mapper.writeValue(Gdx.files.local("creature.json").file(), creature);
        } catch (IOException e) {
            e.printStackTrace();
        }
    
        //To map it back to a object we do the same
        Creature jsonCreature = null;
        try {
            jsonCreature = mapper.readValue(Gdx.files.local("creature.json").readString(), Creature.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    
        //Jackson also has control over what you want to serialize
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        //Or with annotation in front of the class
        @JsonIgnoreProperties({"nameOfProperty", "anotherProperty"})
    

    This all at least gives me the same power as the build in json serializer and it serializes EnumSet right off the bat.

    If someone knows how to de-serialize my write method in the initial question I will be glad to accept that as the answer.