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.