2

I'll preface this with the fact that I'm trying to avoid using Newtonsoft.Json since, ostensibly, System.Text.Json is ready for prime-time in .NET 6.

So I have two enums coming from an API and I want to deserialise them using this test method:

[Theory]
[ClassData(typeof(TestDataGenerator))]
public void CanDeserialiseEnumsWithCustomJsonStrings(Enum expected, string jsonName)
{
    jsonName.ShouldNotBeNullOrEmpty();
    ReadOnlySpan<char> json = $"{{\"TestEnum\":\"{jsonName}\"}}";

    Type constructed = typeof(TestEnumWrapper<>).MakeGenericType(expected.GetType());
        
    var res = JsonSerializer.Deserialize(json, constructed);

    constructed.GetProperty("TestEnum").GetValue(res).ShouldBe(expected);
}

private class TestEnumWrapper<T> where T: struct
{
    public T TestEnum { get; set; }
}

(Yes I know that this could be done with JsonSerializer.Deserialize<T>(), I want to be able to test many types with this test so I need the reflection AFAICT).

The first one, works fine:

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum RecordType
{
        [JsonPropertyName("country")]
        Country = 1,

        [JsonPropertyName("destinationOrbit")]
        DestinationOrbit = 2,

        [JsonPropertyName("engine")]
        Engine = 3,
        //etc...

}

The second one, fails on the deserialization, this seems to be due to the spaces in the names.

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ObjectClass
{
    [JsonPropertyName("Rocket Body")]
    RocketBody,
    [JsonPropertyName("Rocket Debris")]
    RocketDebris,
    [JsonPropertyName("Rocket Fragmentation Debris")]
    RocketFragmentationDebris,
    [JsonPropertyName("Rocket Mission Related Object")]
    RocketMissionRelatedObject,
    //etc...
}

The API is controlled by the European Space Agency, so somehow, I don't think I'll be able to persuade them to rationalise the response a bit more.

Is there any way around this?


Some have asked for an example of the JSON I'm trying to deserialise. I'm currently working on the Attributes part of this blob:

{
            "type": "object",
            "attributes": {
                "shape": null,
                "xSectMin": null,
                "satno": null,
                "depth": null,
                "objectClass": "Rocket Fragmentation Debris",
                "cosparId": null,
                "length": null,
                "height": null,
                "mass": null,
                "xSectMax": null,
                "vimpelId": 84303,
                "xSectAvg": null,
                "name": null
            },
            "relationships": {
                "states": {
                    "links": {
                        "self": "/api/objects/61345/relationships/states",
                        "related": "/api/objects/61345/states"
                    }
                },
                "initialOrbits": {
                    "links": {
                        "self": "/api/objects/61345/relationships/initial-orbits",
                        "related": "/api/objects/61345/initial-orbits"
                    }
                },
                "destinationOrbits": {
                    "links": {
                        "self": "/api/objects/61345/relationships/destination-orbits",
                        "related": "/api/objects/61345/destination-orbits"
                    }
                },
                "operators": {
                    "links": {
                        "self": "/api/objects/61345/relationships/operators",
                        "related": "/api/objects/61345/operators"
                    }
                },
                "launch": {
                    "links": {
                        "self": "/api/objects/61345/relationships/launch",
                        "related": "/api/objects/61345/launch"
                    }
                },
                "reentry": {
                    "links": {
                        "self": "/api/objects/61345/relationships/reentry",
                        "related": "/api/objects/61345/reentry"
                    }
                }
            },
            "id": "61345",
            "links": {
                "self": "/api/objects/61345"
            }
        }
Serge
  • 40,935
  • 4
  • 18
  • 45
ScottishTapWater
  • 3,656
  • 4
  • 38
  • 81
  • 1
    Not supported by System.Text.Json. See: [System.Text.Json: How do I specify a custom name for an enum value?](https://stackoverflow.com/q/59059989/3744182), which has some suggested workarounds. – dbc Dec 28 '21 at 00:10
  • Deserializing `RecordType` works because, as explained in the [docs](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter?view=net-6.0#remarks), ***Reading is case insensitive.** Writing can be customized by using a `JsonNamingPolicy`.* Note that it's implied that reading cannot be customized by a `JsonNamingPolicy`. – dbc Dec 28 '21 at 00:15
  • 1
    Works fine in `Newtonsoft.Json`. Don't torture yourself. It's possible in `System.Text.Json` but requires lots of custom code or a Nuget. – Charles Dec 28 '21 at 00:25
  • Can you post the real json that you have pls? It is hard understand what is the problem, why you have to deserialize or serialize enums? – Serge Dec 28 '21 at 00:34
  • @Serge - Added :) – ScottishTapWater Dec 28 '21 at 00:41
  • @Charles - I'm all good with with a nuget if you know one that works well? – ScottishTapWater Dec 28 '21 at 00:41
  • @dbc - I honestly thought this would've been fixed for dotnet 6... That's a shame – ScottishTapWater Dec 28 '21 at 00:42
  • Oh and to answer the second part of your question @Serge, I don't really like leaving things as strings when there's actually a finite number of them... Seems like a textbook case of should-probably-be-an-enum imho – ScottishTapWater Dec 28 '21 at 00:52
  • @Persistence It is ok, I am mostly wondering why you are not allowed to Newtonsoft.Json? System.Text.Json is very tricky and bugy. A half of its featrures is still not working properly. – Serge Dec 28 '21 at 00:58
  • @Serge - There's nothing stopping me using it, it's more that I don't want to if I can help it. With dotnet 6, supposedly Text.Json was ready for prime time, so I was trying to avoid writing newtonsoft dependent stuff if I could help it. If there's no straightforward solution then I'll bin it off – ScottishTapWater Dec 28 '21 at 01:00

2 Answers2

5

This is a solution for System.Text.Json

I used the Nuget package System.Text.Json.EnumExtensions
https://github.com/StefH/System.Text.Json.EnumExtensions

// you probably want these options somewhere global
private JsonSerializerOptions options;

private class TestEnumWrapper<T> where T : struct
{
    public T TestEnum { get; set; }
}

public enum ObjectClass
{
    [EnumMember(Value = "Rocket Body")]
    RocketBody,
    [EnumMember(Value = "Rocket Debris")]
    RocketDebris,
    [EnumMember(Value = "Rocket Fragmentation Debris")]
    RocketFragmentationDebris,
    [EnumMember(Value = "Rocket Mission Related Object")]
    RocketMissionRelatedObject,
    //etc...
}

private void CanDeserialiseEnumsWithCustomJsonStrings(Enum expected, string jsonName)
{
    var json = $"{{\"TestEnum\":\"{jsonName}\"}}";

    Type constructed = typeof(TestEnumWrapper<>).MakeGenericType(expected.GetType());

    var res = JsonSerializer.Deserialize(json, constructed, options);
}


public void Test()
{
    this.options = new JsonSerializerOptions();
    options.Converters.Add(new JsonStringEnumConverterWithAttributeSupport());

    var testSerialize = JsonSerializer.Serialize(new TestEnumWrapper<ObjectClass>() 
        { TestEnum = ObjectClass.RocketBody }, options);

    // Test Deserialize
    CanDeserialiseEnumsWithCustomJsonStrings(ObjectClass.RocketBody, "Rocket Body");
}

You only have to add the new JsonStringEnumConverterWithAttributeSupport() to the JsonSerializerOptions.Converters for this to work.

If you want to use the Converter as an Attribute you have to add an parameterless constructor to the class JsonStringEnumConverterWithAttributeSupport

public JsonStringEnumConverterWithAttributeSupport() : this(namingPolicy : null, allowIntegerValues : true,
    parseEnumMemberAttribute : true, parseDisplayAttribute : false, parseDescriptionAttribute : false)
{

}
Charles
  • 2,721
  • 1
  • 9
  • 15
  • Going to leave it another hour or so to see if someone knows of a decent System.Text.Json option, if they don't, then I'll accept this one :) Although it would be good if you could add a short summary of what you've done because code-only answers aren't rated in these parts (even though I can see what you've done). Cheers bud – ScottishTapWater Dec 28 '21 at 00:44
  • @Persistence i added a version for `System.Text.Json` – Charles Dec 28 '21 at 00:59
  • Thanking you my good sir, you are a scholar and a gent. Does it need the `[JsonConverter(typeof(JsonStringEnumConverterWithAttributeSupport))]` attribute? – ScottishTapWater Dec 28 '21 at 01:06
  • @Persistence it works out of the box with the options like in my example, if you want to use the converter as an Attribute you have to add a parameterless constructor inside the Library class. (I just copied the 5 files from the nuget into my project, so i can edit them) – Charles Dec 28 '21 at 01:16
  • So I've just tested with this code here and I'm actually getting similar results to before I added the nuget, have I missed something? Thank you for being so helpful here https://gist.github.com/hughesjs/2a34b68588fd2fae3935abdda2dea2e4 – ScottishTapWater Dec 28 '21 at 01:17
  • @Persistence uhm it works in my example, if you want me to debug your code, give me a few min. I don't have all your libraries installed. – Charles Dec 28 '21 at 01:20
  • Please don't feel obliged, I was mainly asking in case it was obvious. If you're happy to help though, I'd really appreciate it. – ScottishTapWater Dec 28 '21 at 01:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240494/discussion-between-persistence-and-charles). – ScottishTapWater Dec 28 '21 at 01:24
  • Added a PR to upstream for your change https://github.com/StefH/System.Text.Json.EnumExtensions/pull/7 – ScottishTapWater Dec 28 '21 at 01:49
2

try this, I uses Newtonsoft.Json and for test I deserialized only attributes since only they contain enum. You don't need any custom code.

var attributes= JsonConvert.DeserializeObject<Root>(json);

classes


    public enum ObjectClass
    {
    [EnumMember(Value = "Rocket Body")]
    RocketBody,
    [EnumMember(Value ="Rocket Debris")]
    RocketDebris,
    [EnumMember(Value = "Rocket Fragmentation Debris")]
    RocketFragmentationDebris,
    [EnumMember(Value ="Rocket Mission Related Object")]
    RocketMissionRelatedObject
    }

    public partial class Root
    {
        [JsonProperty("attributes")]
        public Attributes Attributes { get; set; }
    }

    public partial class Attributes
    {
        [JsonProperty("shape")]
        public object Shape { get; set; }

        [JsonProperty("xSectMin")]
        public object XSectMin { get; set; }

        [JsonProperty("satno")]
        public object Satno { get; set; }

        [JsonProperty("depth")]
        public object Depth { get; set; }

        [JsonProperty("objectClass")]
        public ObjectClass ObjectClass { get; set; }

        [JsonProperty("cosparId")]
        public object CosparId { get; set; }

        [JsonProperty("length")]
        public object Length { get; set; }

        [JsonProperty("height")]
        public object Height { get; set; }

        [JsonProperty("mass")]
        public object Mass { get; set; }

        [JsonProperty("xSectMax")]
        public object XSectMax { get; set; }

        [JsonProperty("vimpelId")]
        public long VimpelId { get; set; }

        [JsonProperty("xSectAvg")]
        public object XSectAvg { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }
    }

test

json=JsonConvert.SerializeObject(attributes); 
attributes= JsonConvert.DeserializeObject<Root>(json);

result

{
  "attributes": {
    "shape": null,
    "xSectMin": null,
    "satno": null,
    "depth": null,
    "objectClass": 2,
    "cosparId": null,
    "length": null,
    "height": null,
    "mass": null,
    "xSectMax": null,
    "vimpelId": 84303,
    "xSectAvg": null,
    "name": null
  }
}

Serge
  • 40,935
  • 4
  • 18
  • 45