1
\$\begingroup\$

I am working on and entity component system and I am storing entities as a JSON file. Components are stored in an entity as a Dictionary of components. I am using newtonsoft's JSON library.

After much frustration I managed to serialize a dictionary that would store objects derived from Component using the ItemTypeNameHandling attribute

Component class:

 class Component
{
    protected String name;
    public String Name { get { return name; } }
}

Example of a derived component:

[JsonObject(MemberSerialization.OptIn)]
class Position : Component
{
    [JsonProperty(Required = Required.Always)]
    public int x = 50;
    [JsonProperty(Required = Required.Always)]
    public int y = 20;

    public Position() { this.name = "position"; }
}

Entity class with Dictionary:

class Entity
{
    ...
    [JsonProperty(Required = Required.Always, ItemTypeNameHandling = TypeNameHandling.Objects)]
    private Dictionary<String, Component> components = new Dictionary<String, Component>();
    ...
}

However I am not satisfied with how the JSON looks. I get this:

"components": {
"position": {
  "$type": "Project_Citrus.Engine.Components.Position, Project Citrus",
  "x": 50,
  "y": 20
 },
"health": {
  "$type": "Project_Citrus.Engine.Components.Health, Project Citrus",
  "HP": 100
 }
}

I want to be easily able to edit my JSON files to create new content. Having to put "$type": "..." makes it seem rather cumbersome and having someone who is not familiar with my object library would have a tough time adding new components. Is there some way to create something like below and still be able to de serialize and de serialize using the component name? Here's an example of what I would like to have.

"components": {
"position": {
  "x": 50,
  "y": 20
 },
"health": {
  "HP": 100
 }
}
\$\endgroup\$
  • \$\begingroup\$ Well, you need the type, otherwise deserialization won't know what class instances to create? Or how do you plan to deserialize the JSON? \$\endgroup\$ – bummzack Oct 31 '14 at 7:08
  • \$\begingroup\$ I actually ended up finding out how to do this, I created a custom converter. \$\endgroup\$ – Ziamor Oct 31 '14 at 20:05
  • \$\begingroup\$ You should write an answer. \$\endgroup\$ – wolfdawn Nov 2 '14 at 7:10
2
\$\begingroup\$

After a few hours of work I found I could create a custom converter by extending JsonConverter. I used this answer to help me out.

I got my output to look like:

  "components": [
     {
       "name": "position",
       "x": 50,
       "y": 20
     },
     {
      "name": "health",
      "HP": 100
     }
   ]

I added my converter as an attribute to the dictionary like so:

[JsonProperty(Required = Required.Always), JsonConverter(typeof(DictionaryComponentConverter))]
    private Dictionary<String, Component> components = new Dictionary<String, Component>();

I then did the same to the Component class though I created a new converter ComponentConverter:

[JsonObject(MemberSerialization.OptIn), JsonConverter(typeof(ComponentConverter))]
class Component
{
    protected String name;
    public String Name { get { return name; } }
}

It's not how I want the code too look like in the end, Im going to try to clean it but heres how my converters look like:

DictionaryComponentConverter:

class DictionaryComponentConverter : JsonConverter
{

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.StartArray)
        {
            // Load JArray from stream
            JArray jArray = JArray.Load(reader);

            // Create target object based on JObject
            List<Component> target = new List<Component>();

            // Populate the object properties
            serializer.Populate(jArray.CreateReader(), target);
            Dictionary<String, Component> dictionary = new Dictionary<string, Component>();
            foreach (Component comp in target)
                if (!dictionary.ContainsKey(comp.Name))
                    dictionary.Add(comp.Name, comp);
            return dictionary;
        }
        return new Dictionary<string, Component>();
    }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value is Dictionary<String, Component>)
        {
            Dictionary<String, Component> components = (Dictionary<String, Component>)value;
            writer.WriteStartArray();
            foreach (KeyValuePair<String, Component> entry in components)
            {
                serializer.Serialize(writer, entry.Value);
            }
            writer.WriteEndArray();
        }
        else
        {
            writer.WriteStartObject();
            writer.WriteEndObject();
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Dictionary<string, Component>).IsAssignableFrom(objectType);
    }
}

ComponentConverter:

class ComponentConverter : JsonConverter
{
    protected Component Create(Type objectType, JObject jObject)
    {
        if (jObject["name"] != null)
            if (jObject["name"].ToString() == "position")
            {
                return new Position();
            }
            else if (jObject["name"].ToString() == "health")
            {
                return new Health();
            }

        return new Component();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        Component target = Create(objectType, jObject);

        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value is Position)
        {
            Position position = (Position)value;
            writer.WriteStartObject();
            writer.WritePropertyName("name");
            writer.WriteValue(position.Name);
            writer.WritePropertyName("x");
            writer.WriteValue(position.x);
            writer.WritePropertyName("y");
            writer.WriteValue(position.y);
            writer.WriteEndObject();
        }
        else if (value is Health)
        {
            Health health = (Health)value;
            writer.WriteStartObject();
            writer.WritePropertyName("name");
            writer.WriteValue(health.Name);
            writer.WritePropertyName("HP");
            writer.WriteValue(health.HP);
            writer.WriteEndObject();
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Component).IsAssignableFrom(objectType);
    }
}
\$\endgroup\$
2
\$\begingroup\$

There is no one right answer. I open with that because I know there are many who disagree with what I'm about to say. I do use NewtonSoft for JSON serializing and deserializing, but from a somewhat different angle.

After much experience with apis and file formats, I've settled on the approach of designing the file format (or network message format, in some cases) completely separately from any runtime objects. I never want to just "serialize my object graph".

I implement the loader and saver as separate code that operates using the public APIs of the runtime objects and relatively generic JSON/XML/Whatever readers and writers. The loader and saver of course can become a relatively thick bundle of code, full of loops and run-on paragraphs.

But! it is extremely testable, so that bundle is quite safe, if you have reasonable unit tests.

This approach becomes especially valuable when dealing with backward compatibility and versioning, new fields (sometimes beyond a simple "default value"), working around quirks of earlier implementations or even external tools that read and write this file format, and so on, in a highly flexible manner. Again, unit tests & TDD are a perfect fit here for defining and enforcing the desired behavior.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.