I have the following C# class
[DataContract(Name = "Person")]
public sealed class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public List<int> Numbers { get; set; }
}
The object created with
Person person = new Person
{
Name = "Test",
Numbers = new List<int> { 1, 2, 3 }
};
is serialized to an XML document with the DataContractSerializer
:
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization">
<Name>Test</Name>
<Numbers xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:int>1</d2p1:int>
<d2p1:int>2</d2p1:int>
<d2p1:int>3</d2p1:int>
</Numbers>
</Person>
JsonConvert.SerializeXNode(xmlDocument)
returns the following JSON:
{
"Person": {
"@xmlns:i": "http://www.w3.org/2001/XMLSchema-instance",
"@xmlns": "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization",
"Name": "Test",
"Numbers": {
"@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
"d2p1:int": [
"1",
"2",
"3"
]
}
}
}
When I deserialize the JSON above with JsonConvert.DeserializeObject(json, typeof(Person))
to a POCO both Properties (Name
, Numbers
) are NULL
.
I've already tried to remove the outer object:
{
"@xmlns:i": "http://www.w3.org/2001/XMLSchema-instance",
"@xmlns": "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization",
"Name": "Test",
"Numbers": {
"@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
"d2p1:int": [
"1",
"2",
"3"
]
}
}
Then the following exception occurs:
Newtonsoft.Json.JsonSerializationException
HResult=0x80131500
Nachricht = Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[System.Int32]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'Numbers.@xmlns:d2p1', line 6, position 18.
Quelle = Newtonsoft.Json
Stapelüberwachung:
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
It seems that the JSON converted from an XML document differs from the JSON directly created from the POCO.
POCO => JSON => POCO works fine
POCO => XML => JSON => POCO The POCO gets NULL properties and/or the exception
Is it possible to configure Json.NET so that it creates compatible JSON documents?
You can write a custom JsonConverter
to deserialize JSON transcoded from XML generated by DataContractSerializer
for some List<T>
, but it will be quite elaborate. Taking into account the "Conversion Rules" specified in the Newtonsoft documentation Converting between JSON and XML:
Because multiple nodes with the same name at the same level are grouped together into an array, the conversion process can produce different JSON depending on the number of nodes.
The following four cases must be handled:
A list value with multiple items will look like:
{
"@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
"d2p1:int": [
"1",
"2",
"3"
]
}
or (without a namespace):
{
"int": [
"1",
"2",
"3"
]
}
A list with a single value will look like:
{
"@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
"d2p1:int": "1"
}
or (without a namespace):
{
"int": "1"
}
An empty list will look like:
{
"@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays"
}
or (no namespace):
null
(Yes, this is the correct transcoding of <Numbers />
.)
A null
list will look like:
{
"@xmlns:d2p1": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
"@i:nil": "true"
}
Or
{
"@i:nil": "true"
}
The following converter should handle all the cases, for all List<T>
values for any T
that is not itself a collection:
public class ListFromDataContractXmlConverter : JsonConverter
{
readonly IContractResolver resolver;
public ListFromDataContractXmlConverter() : this(null) { }
public ListFromDataContractXmlConverter(IContractResolver resolver)
{
// Use the global default resolver if none is passed in.
this.resolver = resolver ?? new JsonSerializer().ContractResolver;
}
static bool CanConvert(Type objectType, IContractResolver resolver)
{
Type itemType;
JsonArrayContract contract;
return CanConvert(objectType, resolver, out itemType, out contract);
}
static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
{
if ((itemType = objectType.GetListItemType()) == null)
{
itemType = null;
contract = null;
return false;
}
// Ensure that [JsonObject] is not applied to the type.
if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
return false;
var itemContract = resolver.ResolveContract(itemType);
// Not implemented for jagged arrays.
if (itemContract is JsonArrayContract)
return false;
return true;
}
public override bool CanConvert(Type objectType) { return CanConvert(objectType, resolver); }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Type itemType;
JsonArrayContract contract;
if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
var list = (IList)(existingValue ?? contract.DefaultCreator());
switch (reader.MoveToContentAndAssert().TokenType)
{
case JsonToken.Null:
// This is how Json.NET transcodes an empty DataContractSerializer XML list when no namespace is present.
break;
case JsonToken.StartArray:
{
serializer.Populate(reader, list);
}
break;
case JsonToken.StartObject:
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
var name = (string)reader.AssertTokenType(JsonToken.PropertyName).Value;
reader.ReadToContentAndAssert();
if (name.StartsWith("@"))
{
// It's an attribute.
if (!name.StartsWith("@xmlns:") && name.EndsWith("nil") && reader.TokenType == JsonToken.String && ((string)reader.Value) == "true")
// It's not a namespace, it's the {http://www.w3.org/2001/XMLSchema-instance}nil="true" null indicator attribute.
list = null;
}
else
{
// It's an element.
if (reader.TokenType == JsonToken.StartArray)
// The list had multiple items
serializer.Populate(reader, list);
else
// The list had a single item. This is it.
list.Add(serializer.Deserialize(reader, itemType));
}
}
break;
default:
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
}
return list;
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}
public static partial class JsonExtensions
{
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
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();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
public static Type GetListItemType(this Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
Then, to transcode and deserialize, do:
var xelement = XElement.Parse(xmlString);
var jsonFromXml = JsonConvert.SerializeXNode(xelement, Formatting.None, omitRootObject: true);
var settings = new JsonSerializerSettings
{
Converters = { new ListFromDataContractXmlConverter() },
};
var deserializedPerson = JsonConvert.DeserializeObject<Person>(jsonFromXml, settings);
Notes:
The converter is not implemented for jagged lists (i.e. any List<T>
where T
is some IEnumerable
other that string, such as List<List<int>>
or List<int []>
).
To implement jagged lists, it would be necessary to compare the JSON for a list containing one item that has multiple values with the JSON for a list containing multiple items each with one value with the JSON for a list with one value that itself has one value and ensure that these cases can be distinguished successfully with and without the presence of a namespace.
Without omitRootObject: true
the intermediate JSON will have an extra level of object nesting
{ "Person": { /* The person contents */ } }
You would need to add an extra level of wrapper object to your data model to deserialize such JSON e.g.:
public class RootObject { public Person Person { get; set; } }
Parsing to XElement
rather than XDocument
strips the XML declaration from the transcoded JSON, which otherwise could cause problems when deserializing with omitRootObject: true
.
It would be much easier to simply deserialize your Person
directly from XML using DataContractSerializer
. You can do this as long as you add the correct namespace to the data contract attribute (which is only necessary if it differs from your default namespace):
[DataContract(Name = "Person", Namespace = "http://schemas.datacontract.org/2004/07/Workflows.MassTransit.Hosting.Serialization")]
public sealed class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public List<int> Numbers { get; set; }
}
See https://dotnetfiddle.net/Q46exx for a demo.
Demo fiddle here: https://dotnetfiddle.net/9B7Sdn.