1

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.

nuk
  • 39
  • 6
  • _"Why does json, the superior format, seem to struggle with it so badly?"_ - .NET enums are first and foremost a numeric type. Libraries such as JSON.NET and System.Text.Json allow you to create `JsonConverter` classes that can handle conversions between JSON representations and their .NET models. For string enum values, this is `StringEnumConverter` for JSON.NET and `JsonStringEnumConverter` for `System.Text.Json`. I don't know if Unity has something similar with its `JsonUtility`. Even so, I think you'd still need a custom converter for the case of bitflag -> string. – ProgrammingLlama Jul 31 '23 at 03:18
  • 2
    `Enum.Parse` can handle `"Forest, Cave"`, which might translate over to the default json parsing rules. Otherwise you'd need a custom converter. – Jeremy Lakeman Jul 31 '23 at 03:27
  • If you do not want to create a custom converter I would consider packing/unpacking your value to a `List` . – JonasH Jul 31 '23 at 08:35

1 Answers1

4

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
derHugo
  • 83,094
  • 9
  • 75
  • 115