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.