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.
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.