2

I'm working on a library to parse Minecraft JSON files. However, they have a slightly weird format that I can't figure out how to parse with Newtonsoft's JSON.net:

{
    "values": [
        "some_object",
        "another_object",
        {
            "name": "third_object",
            "required": false
        }
    ]
}

The class I'm trying to deserialize the values array into looks like this:

class TagValue
{
    public string Name;
    public bool Required = false;
}

So for values which are just a string, it should set Name to the string and leave Required as false.

Is there a way I can do this? I'm very new to the library and can't figure out how to do anything more advanced than the most basic deserialization.

Emperor Eto
  • 2,456
  • 2
  • 18
  • 32
devvoid
  • 143
  • 1
  • 8
  • Didn't change anything. Having `Required` default to false isn't the issue, it's deserializing a string directly into a TagValue object. – devvoid Mar 25 '23 at 12:35
  • Do you know the values are always either going to be strings or TagValue? – Emperor Eto Mar 25 '23 at 12:36
  • No, the JSON files mix and match them constantly like in the example given – devvoid Mar 25 '23 at 12:36
  • Right but is it always going to be either one or the other or are there other object types that could be mixed in there? If you know the universe of possibilities then you can handle this with a custom JsonConverter – Emperor Eto Mar 25 '23 at 12:38
  • Oh sorry, misread what you meant. Yes, it's always going to be either a string or an object like what's described, those are the only two data types. – devvoid Mar 25 '23 at 12:39
  • Does this answer your question? [How to handle both a single item and an array for the same property using JSON.net](https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n) – Leandro Bardelli Mar 25 '23 at 12:42
  • 1
    have you already checked this nice code generator for the JsonConverter? https://app.quicktype.io/?l=csharp – Falco Alexander Mar 25 '23 at 12:45
  • @devvoid perfect see below. – Emperor Eto Mar 25 '23 at 12:48

2 Answers2

2

Try this

[JsonConverter(typeof(TagValueConverter))]
class TagValue
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("required")]
    public bool Required { get; set; }

    private class TagValueConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(string) ||
                objectType == typeof(TagValue);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.String)
                return new TagValue
                {
                    Name = reader.Value as string
                };
            else
            {
                var jobj = JObject.Load(reader);
                var obj = new TagValue();
                serializer.Populate(jobj.CreateReader(), obj);
                return obj;
            }
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
}

Then presumably you have/need a class for the outer object:

class TagValues
{
    [JsonProperty("values")]
    public TagValue[] Values { get; set; }

    public static void TestMe()
    {
        string json = @"{
            ""values"": [
                ""some_object"",
                ""another_object"",
                {
                    ""name"": ""third_object"",
                    ""required"": true
                }
            ]
        }";

        var tags = JsonConvert.DeserializeObject<TagValues>(json);

        Debug.Assert(tags.Values?.Length == 3);
        Debug.Assert(tags.Values[0].Name == "some_object");
        Debug.Assert(tags.Values[1].Name == "another_object");
        Debug.Assert(tags.Values[2].Name == "third_object" &&
                    tags.Values[2].Required == true);
    }
}

PS - You also should make those fields properties as was suggested in the comments.

Emperor Eto
  • 2,456
  • 2
  • 18
  • 32
0

you don't need any converters, just one string of code is plenty

    List<TagValue> values = JObject.Parse(json)["values"]
                .Select(va => va.Type == JTokenType.Object
                ? va.ToObject<TagValue>()
                : new TagValue { Name = (string) va }
                ).ToList();
Serge
  • 40,935
  • 4
  • 18
  • 45
  • Of course this works if it's a one-time deserialization but it doesn't really scale. Associating the custom `JsonConverter` with the class itself allows you to deserialize normally anywhere and not have to think about it. I think it's a heavily under-utilized feature of Json.NET IMHO. – Emperor Eto Mar 31 '23 at 20:03
  • @It can be made as method and used the same way as converter. Your it working with only one class TagValue and can not be used everywhere. It is just overcomplicated. – Serge Mar 31 '23 at 20:09
  • And if this object later appears as a member of another class then that class would also need a custom deserialization method, etc. Or what if this class were a body argument in a web API? Truly the converter is the best practice, I'm surprised anyone would disagree. – Emperor Eto Mar 31 '23 at 21:52