Search code examples
c#jsonunity-game-engineenumsbitflags

Deserializing a bitflag enum in C# with Unity


I have a unity project. Using the stock json tools at my disposal, i'm trying to deserialize an enum that uses bit flags (ie something like)

[Flags]
enum Terrain 
{
  NORMAL = 0,
  FOREST = 1,
  SWAMP = 2,
  CAVE = 4 
}

In json it's something like CityTerrain: "FOREST", however it seems that the deserialization doesn't like human readable string enums (it wants an integer i assume) which is especially troubling for me in the case of bit flags when i want to be able to combine flags, ie. FOREST|CAVE.

XML handled bitflag enums fine out of the box. Why does json, the superior format, seem to struggle with it so badly? Should i be making an enum wrapper for handling the deserialization of this? No matter what i do, i feel like my solution will be clunky and i will dislike it.

Ultimately it needs to be human readable and i won't accept a "solution" where my bitflag enum has to be represented as 5 in json. The entire point of this exercise is that it needs to be human readable but still deserializable.


Solution

  • For Newtonsoft.Json

    you can use a StringEnumConverter and pass it along into

    var converter = new StringEnumConverter();
        
    var json = JsonConvert.SerializeObject(example, Formatting.Indented, converter);
    

    and (actually optional apparently)

    var example = JsonConvert.DeserializeObject<Example>(json, converter);
    

    in my test for deserializing it seems to also work with only

    var example = JsonConvert.DeserializeObject<Example>(json);
    

    For System.Text.Json

    you can use the equivalent JsonStringEnumConverter and pass it via the options like e.g.

    // IncludeFields depends on whether your type uses fields or properties
    // by default System.Text.Json only considers properties
    // so to reflect the behavior of Newtonsoft I enabled also fields
    var options = new JsonSerializerOptions() { WriteIndented = true,  IncludeFields = true };
    options.Converters.Add(new JsonStringEnumConverter());
        
    var json = JsonSerializer.Serialize(example, options);
    

    and

    var example = JsonSerializer.Deserialize<Example>(json, options);
    

    .Net Fiddle for both above

    Note (also applies to JsonUtility below): Both basically go through ToString and Enum.Parse so in the json it will read e.g.

    "FOREST, SWAMP"
    

    if for some reason you really want FOREST|SWAMP instead you will have to implement your own custom JsonConverter (Newtonsoft) or accordingly JsonConverter (System.Text.Json).

    Basically using the same and do something like (pseudo code)

    // When serializing Terrain -> string
    var enumString = value.ToString().Replace(", ", "|");
    

    and

    // When deserializing string -> Terrain
    var enumValue = (Terrain)Enum.Parse(enumString.Replace("|", ", "));
    

    For built-in JsonUtility

    afaik there is unfortunately nothing similar. You instead have to overwrite the serialization of the entire class like e.g.

    public class Example : ISerializationCallbackReceiver
    {
        public string Name;
        
        [NonSerialized]
        public Terrain Terrain;
        
        public int SomeVlaue;
    
        [SerializeField]
        private string terrain;
        
        
        public void OnBeforeSerialize()
        {
            terrain = Terrain.ToString();
        }
    
        public void OnAfterDeserialize()
        {
            Terrain = (Terrain)Enum.Parse(typeof(Terrain), terrain);
        }
    }
    

    NOTE:

    • Have in mind though that this will also affect the way it is displayed in the Inspector => You might then want to implement a custom Inspector to basically "revert" the display and draw it as an enum flag field again.
    • You also will have to repeat this for each and every type that contains a Terrain and shall be json serialized