1

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.

2
  • 1
    When you say "either XML or JSON" do you actually need to be able to do both? Anyway, duplicate JSON property names is not to spec, and will be ignored by many parsers, so your expected JSON needs a rethink. Commented May 6 at 15:59
  • Yes, I need to be able to create both (XML and JSON). The 3rd-party interface started (and continues to do) with transferring data structures in XML, and the next step will be to move to JSON. So, we need XML for the production environment and JSON for the test environment. At some point in the future, the XML part will likely be deactivated. I think @dbc's answer (using JsonConverter) will solve my problem without having to reimplement all data structures. Commented May 7 at 10:58

1 Answer 1

0

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:

  • No changes are required to your Foo class.

  • You wrote: The data model of the 3rd-party interface is much more complex as this small example.

    While you will need to write a custom converter for FooListResponse, the Foo type can be serialized automatically by invoking the serializer from within the converter. Thus, as long as all the complexity of the real interface is in the Foo class, you are not required to write lots of custom code.

  • Code written using C# 6 syntax in order to be used in .NET Framework where Json.NET is most commonly still used.

  • JSON with repeating property names cannot be loaded into a JToken hierarchy. Either the first or last value will be loaded, or an exception will be thrown, depending on the value of JsonLoadSettings.DuplicatePropertyNameHandling.

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.

Sign up to request clarification or add additional context in comments.

5 Comments

I checked the documentation and the vendor of the interface wants to have the duplicate property names. 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. Thank you very much for your very good answer and the demo.
While implementing the JsonConverter classes for my data structure I found another problem when deserialzing a structure like following: ´´´ [ { "Foo": { "Name": "George", "Colors": [ { "Color": { ... }, "Color": { ... } } ] }, "Foo": { ... } } ] ´´´ The implementation of the method "ReadJson" of the solution only deserialize the first "Color" object. Any hints how to adopt the JsonConverter?
@MichaelHartmann - I would need to see a minimal reproducible example to help you. The format for questions on stack overflow is one question per post, so I'd suggest asking another question. It would be easy enough to generalize the solution to use a generic converter, however that might require setting up your classes in a way which would would break XML serialization. I'd need to see the required XML as well as the required JSON to avoid that -- which would best be addressed in another question.
@MichaelHartmann - here's an attempt to generalize the converter for use in generic cases: dotnetfiddle.net/hPl6Mt. You would use WrappedListAsListOfObjectsWithDuplicateNameConverter<FooListResponse, Foo> for when your list is wrapped in some wrapper container like FooListResponse. You would use ListAsListOfObjectsWithDuplicateNameConverter<T> when it isn't, e.g. a List<Color> property. But I'm only guessing that this would work, you need to ask a new question with a minimal reproducible example for me to answer definitively.
Found the solution by myself and it was no problem of the JsonConverter. We have an extension method for deserializing a data structure from JSON and this method can be used in two different ways. Default should be calling "JsonConvert.DeserializeObject<>" and for some rare conditions we can also switch to use "JObject.Parse(json).ToObject<>". Someone has changed the method, so that "JObject.Parse" will be the default and this caused the problem with embedded lists of objects. Now I call "JsonConvert.DeserializeObject<>" and everything's fine. Thanks for your support!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.