Search code examples
c#jsonjson.net

How to serialize and deserialize a JSON object with duplicate property names in a specific order?


I need to create a C# class that matches this json with the exact brackets like this.

{
    "data": {
        "a": "4",
        "b": "2",
        "c": "3",
        "a": "444",
    },
}

System.Collections.Generic.Dictionary<string, string> does it, but it does not allow multiple entries with the same key, so that won't work.

List<T> with a custom data class, string[2], tuples or valueTuples or KeyValuePair<string, string> creates different json where each entry gets "item1", "item1" or different brackets.

How can I deserialize and serialize this exact JSON? It's a customer's "standard" that I must support.


Solution

  • While not technically malformed, JSON objects with duplicated property names are not recommended by most recent JSON RFC, RFC 8259:

    An object structure is represented as a pair of curly brackets surrounding zero or more name/value pairs (or members)...

    An object whose names are all unique is interoperable in the sense that all software implementations receiving that object will agree on the name-value mappings. When the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only. Other implementations report an error or fail to parse the object, and some implementations report all of the name/value pairs, including duplicates.

    You may wish to suggest an alternate JSON format to your customer, such as an array of single key-value pair objects.

    That being said, if you must serialize and deserialize objects with duplicate properties, Json.NET will not do this out of the box, and you will have to create a custom JsonConverter to do it manually. Since the JSON object's values are all of the same type (here strings), you could bind to a List<KeyValuePair<string, string>>.

    First, define the following model:

    public class Model
    {
        [JsonConverter(typeof(KeyValueListAsObjectConverter<string>))]
        public List<KeyValuePair<string, string>> data { get; } = new ();
    }
    

    And the following generic JsonConverter<List<KeyValuePair<string, TValue>>>:

    public class KeyValueListAsObjectConverter<TValue> : JsonConverter<List<KeyValuePair<string, TValue>>>
    {
        public override List<KeyValuePair<string, TValue>> ReadJson(JsonReader reader, Type objectType, List<KeyValuePair<string, TValue>> existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
                return null;
            reader.AssertTokenType(JsonToken.StartObject);
            var list = existingValue ?? (List<KeyValuePair<string, TValue>>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
            while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
            {
                var name = (string)reader.AssertTokenType(JsonToken.PropertyName).Value;
                var value = serializer.Deserialize<TValue>(reader.ReadToContentAndAssert());
                list.Add(new KeyValuePair<string, TValue>(name, value));
            }
            return list;
        }
    
        public override void WriteJson(JsonWriter writer, List<KeyValuePair<string, TValue>> value, JsonSerializer serializer)
        {
            writer.WriteStartObject();
            foreach (var pair in value)
            {
                writer.WritePropertyName(pair.Key);
                serializer.Serialize(writer, pair.Value);
            }
            writer.WriteEndObject();
        }
    }
    
    public static partial class JsonExtensions
    {
        public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) => 
            reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
        
        public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
            reader.ReadAndAssert().MoveToContentAndAssert();
    
        public static JsonReader MoveToContentAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException();
            if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
                reader.ReadAndAssert();
            while (reader.TokenType == JsonToken.Comment) // Skip past comments.
                reader.ReadAndAssert();
            return reader;
        }
    
        public static JsonReader ReadAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException();
            if (!reader.Read())
                throw new JsonReaderException("Unexpected end of JSON stream.");
            return reader;
        }
    }
    

    And you will be able to deserialize and re-serialize the JSON shown in your question.

    Demo fiddle here.