json.netxmlserializationjson.net

Problem with serializing object structure with Newtonsoft Json.NET


I have to serialize a data structure for a 3rd-party interface either to XML or to JSON. The expected result should look like the following (simplified) example:

XML:
<?xml version=\"1.0\" encoding=\"utf-8\"?>
<FooList>
    <Foo>
        <Name>George</Name>
        <Color>Blue</Color>
    </Foo>
    <Foo>
        <Name>Betty</Name>
        <Color>Green</Color>
    </Foo>
</FooList>

JSON:
[
  {
    "Foo": {
      "Name": "George",
      "Color": "Blue"
    },
    "Foo": {
      "Name": "Betty",
      "Color": "Green"
    }
  }
]

So I created the following two classes:

[Serializable, XmlType(AnonymousType = true), XmlRoot(Namespace = "", ElementName = "FooList", IsNullable = false)]
public class FooListResponse
{
    [XmlElement("Foo", IsNullable = false)]
    public List<Foo> FooList { get; set; }
}

[Serializable]
public class Foo
{
    [XmlElement("Name"), JsonProperty("Name")]
    public string Name { get; set; }

    [XmlElement("Color"), JsonProperty("Color")]
    public string Color { get; set; }
}

The result of serialization to XML looks as expected, but the result of serialization to JSON with Newtonsoft JSON.Net is not as expected:

{
    "FooList": [
        {
            "Name": "George",
            "Color": "Blue"
        },
        {
            "Name": "Betty",
            "Color": "Green"
        }
    ]
}

I use the method JsonConvert.SerializeObject to serialize the data structure to JSON.

Because the expected JSON starts with a square bracket I also test to serialize only the property 'FooList' (and not the complete object of type 'FooListResponse'). The result is different (no surprise), but still not as expected:

[
    {
        "Name": "George",
        "Color": "Blue"
    },
    {
        "Name": "Betty",
        "Color": "Green"
    }
]

One pair of curly brackets is still missing and the text 'Foo:' in front of each object in the list is missing. Maybe it's a problem of JSON-attributes in my data model or I have to use something different for serializing the data model to JSON.

I hope that the solution for my problem is not "Duplicate all classes in data model for serialization to JSON". The data model of the 3rd-party interface is much more complex as this small example.


Update: I checked the documentation and the vendor of the interface wants to have the duplicate property names (here "Foo"). Normally a collection of objects is serialized to an array structure (square brackets) and all objects in the collection are serialized without a name in front. I don't know, why they decided to use the duplicate names, but when it is possible to deserialize the JSON to a data structure in their software I have no choice.


Solution

  • As mentioned by Charlieface in comments, your required JSON has duplicated property names within a single object, specifically the name "Foo":

    [
      {
        "Foo": { /* Contents of Foo */ },
        "Foo": { /* Contents of Foo */ }
      }
    ]
    

    While JSON containing duplicated property names is not malformed, JSON RFC 8259 recommends against it:

    The names within an object SHOULD be unique.

    You should double-check the documentation of your 3rd-party interface to make sure it really requires duplicated property names.

    That being said, if you are certain you need to generate duplicated property names, you will need to write a custom JsonConverter<FooListResponse> because Json.NET will never create a serialization contract with duplicated names:

    public class FooListResponseConverter : JsonConverter<FooListResponse>
    {
        public override void WriteJson(JsonWriter writer, FooListResponse value, JsonSerializer serializer)
        {
            var list = value == null ? null : value.FooList;
            if (list == null)
            {
                // TODO: decide what to do for a null list.  Maybe write an empty array?
                writer.WriteNull();
                return;
            }
            writer.WriteStartArray();
            writer.WriteStartObject();
            foreach (var foo in list)
            {
                writer.WritePropertyName("Foo"); // in more current C# versions, use nameof(FooListDeserializationDto.Foo)
                serializer.Serialize(writer, foo);
            }
            writer.WriteEndObject();
            writer.WriteEndArray();
        }
    
        class FooListDeserializationDto
        {
            [JsonIgnore] public readonly List<Foo> FooList = new List<Foo>();
            // Here we take advantage of the fact that, if Json.NET encounters duplicated property names during deserialization, it will call the setter multiple times.
            public Foo Foo { set { FooList.Add(value); } }
        }
    
        public override FooListResponse ReadJson(JsonReader reader, Type objectType, FooListResponse existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            var dtoList = serializer.Deserialize<List<FooListDeserializationDto>>(reader);
            if (dtoList == null)
            {
                // TODO: decide what to do for a null list.  Maybe return an empty FooListResponse?
                return null;
            }
            var list = dtoList.Count == 1 
                 ? dtoList[0].FooList
                 : dtoList.SelectMany(d => d.FooList).ToList();
            var fooListResponse = existingValue ?? new FooListResponse();
            fooListResponse.FooList = list;
            return fooListResponse;
        }
    }
    

    Then apply it to FooListResponse as follows:

    [Newtonsoft.Json.JsonConverter(typeof(FooListResponseConverter))] // I added the full namespace to clarify this is not System.Text.Json.JsonConverter
    [Serializable, XmlType(AnonymousType = true), XmlRoot(Namespace = "", ElementName = "FooList", IsNullable = false)]
    public class FooListResponse
    {
        [XmlElement("Foo", IsNullable = false)]
        public List<Foo> FooList { get; set; }
    }
    

    And the your FooListResponse can now be round-tripped to JSON with in the format shown in your question.

    Notes:

    Demo fiddle here.


    Update

    If you need a generic converter because you have many different types each with a single repeating property name, e.g. "Colors": [ { "Color": "Blue", "Color": "Yellow" } ], you will need a way to specify the type and repeating property in runtime:

    public class WrappedListAsListOfObjectsWithDuplicateNameConverter<TWrapper, TItem> : JsonConverter<TWrapper> where TWrapper : new()
    {
        // Apply when you have a wrapper object containing a single List<TItem> property which is serialized as an object with a single property whose value 
        // is an object with repeating property names that is wrapped in some container array:
        readonly string name;
        
        public WrappedListAsListOfObjectsWithDuplicateNameConverter(string name) 
        {
            if (name == null)
                throw new ArgumentNullException("name");
            this.name = name;
        }
    
        static void GetContractAndProperty(JsonSerializer serializer, Type objectType, out JsonObjectContract contract, out JsonProperty property)
        {
            var c = serializer.ContractResolver.ResolveContract(objectType);
            if (!(c is JsonObjectContract))
                throw new JsonSerializationException(string.Format("Contract for {0} was of wrong type {1}", objectType, c.GetType()));
            contract = (JsonObjectContract)c;
            property = contract.Properties.Where(p => !p.Ignored).Single();
            if (!typeof(List<TItem>).IsAssignableFrom(property.PropertyType))
                throw new JsonSerializationException(string.Format("Invalid type {0} for property {1}", property.PropertyType, property.UnderlyingName));
        }
        
        public override void WriteJson(JsonWriter writer, TWrapper value, JsonSerializer serializer)
        {
            JsonObjectContract contract;
            JsonProperty property;
            GetContractAndProperty(serializer, typeof(TWrapper), out contract, out property);
            if (value == null)
            {
                writer.WriteNull();
                return;
            }
            JsonExtensions.WriteListAsListOfObjectsWithDuplicateName<TItem>(writer, (List<TItem>)property.ValueProvider.GetValue(value), serializer, name);
        }
    
        public override TWrapper ReadJson(JsonReader reader, Type objectType, TWrapper existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            JsonObjectContract contract;
            JsonProperty property;
            GetContractAndProperty(serializer, typeof(TWrapper), out contract, out property);
            var existingListValue = hasExistingValue && existingValue != null 
                ? (List<TItem>)property.ValueProvider.GetValue(existingValue)
                : null;
            var list = JsonExtensions.ReadListAsListOfObjectsWithDuplicateName<TItem>(reader, property.PropertyType, existingListValue, serializer, name);
            var value = hasExistingValue && existingValue != null ? existingValue : (TWrapper)contract.DefaultCreator();
            property.ValueProvider.SetValue(value, list);
            return value;
        }
    }
    
    public class ListAsListOfObjectsWithDuplicateNameConverter<T> : JsonConverter<List<T>>
    {
        // Apply when you have a List<T> which is serialized as a single object with repeating property names that is wrapped in some container array:
        readonly string name;
        
        public ListAsListOfObjectsWithDuplicateNameConverter(string name) 
        {
            if (name == null)
                throw new ArgumentNullException("name");
            this.name = name;
        }
    
        public override void WriteJson(JsonWriter writer, List<T> value, JsonSerializer serializer)
        {
            JsonExtensions.WriteListAsListOfObjectsWithDuplicateName<T>(writer, value, serializer, name);
        }
    
        public override List<T> ReadJson(JsonReader reader, Type objectType, List<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            return JsonExtensions.ReadListAsListOfObjectsWithDuplicateName<T>(reader, objectType, existingValue, serializer, name);
        }
    }
    
    public class ListAsObjectWithDuplicateNameConverter<T> : JsonConverter<List<T>>
    {
        // Apply when you have a List<T> which is serialized as a single object with repeating property names
        readonly string name;
        
        public ListAsObjectWithDuplicateNameConverter(string name) 
        {
            if (name == null)
                throw new ArgumentNullException("name");
            this.name = name;
        }
    
        public override void WriteJson(JsonWriter writer, List<T> value, JsonSerializer serializer)
        {
            JsonExtensions.WriteListAsObjectWithDuplicateName<T>(writer, value, serializer, name);
        }
    
        public override List<T> ReadJson(JsonReader reader, Type objectType, List<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            return JsonExtensions.ReadListAsObjectWithDuplicateName<T>(reader, objectType, existingValue, serializer, name);
        }
    }
    
    public static partial class JsonExtensions
    {
        static List<T> CreateList<T>(Type objectType, List<T> existingValue, JsonSerializer serializer)
        {
            return existingValue ?? (objectType == typeof(List<T>) 
                                     ? new List<T>() 
                                     : (List<T>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
        }
        
        public static void WriteListAsListOfObjectsWithDuplicateName<T>(JsonWriter writer, List<T> value, JsonSerializer serializer, string name)
        {
            if (value == null)
            {
                writer.WriteNull();
                return;
            }
            writer.WriteStartArray();
            WriteListAsObjectWithDuplicateName<T>(writer, value, serializer, name);
            writer.WriteEndArray();
        }
        
        public static List<T> ReadListAsListOfObjectsWithDuplicateName<T>(JsonReader reader, Type objectType, List<T> existingValue, JsonSerializer serializer, string name)
        {
            if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
                return null;
            else if (reader.TokenType != JsonToken.StartArray)
                throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            var list = CreateList(objectType, existingValue, serializer);
            while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
                // When an existing list is passed in, ReadListAsObjectWithDuplicateName() appends to it.
                ReadListAsObjectWithDuplicateName<T>(reader, objectType, list, serializer, name);
            return list;
        }
        
        public static void WriteListAsObjectWithDuplicateName<T>(JsonWriter writer, List<T> value, JsonSerializer serializer, string name)
        {
            if (value == null)
            {
                writer.WriteNull();
                return;
            }
            writer.WriteStartObject();
            foreach (var foo in value)
            {
                writer.WritePropertyName(name);
                serializer.Serialize(writer, foo);
            }
            writer.WriteEndObject();
        }
    
        public static List<T> ReadListAsObjectWithDuplicateName<T>(JsonReader reader, Type objectType, List<T> existingValue, JsonSerializer serializer, string name)
        {
            if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
                return null;
            else if (reader.TokenType != JsonToken.StartObject)
                throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
        
            var list = CreateList(objectType, existingValue, serializer);
            while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
            {
                if (reader.TokenType != JsonToken.PropertyName)
                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
                var propertyName = (string)reader.Value;
                reader.ReadAndAssert();
                if (!string.Equals(name, propertyName, StringComparison.OrdinalIgnoreCase))
                {
                    // TODO: decide whether to skip the values of unexpected property names, or throw an exception.
                    // reader.Skip();
                    throw new JsonSerializationException(string.Format("Unexpected property name {0}", propertyName));
                }
                else
                    list.Add(serializer.Deserialize<T>(reader));
            }
            
            return list;
        }
    
        public static JsonReader ReadToContentAndAssert(this JsonReader reader)
        {
            return reader.ReadAndAssert().MoveToContentAndAssert();
        }
    
        public static JsonReader MoveToContentAndAssert(this JsonReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            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("reader");
            if (!reader.Read())
                throw new JsonReaderException("Unexpected end of JSON stream.");
            return reader;
        }
    }
    

    Then FooListResponseConverter becomes:

    public class FooListResponseConverter : WrappedListAsListOfObjectsWithDuplicateNameConverter<FooListResponse, Foo>
    {
        public FooListResponseConverter() : base("Foo") { }
    }
    

    And for Colors you could apply the converter via attributes as follows:

    [XmlElement("Color")]
    [JsonConverter(typeof(ListAsListOfObjectsWithDuplicateNameConverter<string>), 
                   new object [] { "Color" })]
    public List<string> Colors { get; set; }
    

    Demo fiddle #2 here.