I am writing a WCF service that generates various XML and JSON formats for multiple clients. The code below generates a SerializationException: 'TPH_PriceListJsonItems' is a collection type and cannot be serialized when assigned to an interface type that does not implement IEnumerable ('TPH_IPriceListItems'). The XML part is working fine, but not JSON. I do not understand the error, my interface is implementing IEnumerable to represent a class wrapping a simple List<> so I can use the CollectionDataContract.
public class ReproduceDataContractIssue
{
public static void Main(String[] args)
{
// Create test object - vacation products lowest prices grid
TPH_IPriceList priceList = new TPH_PriceListJson();
priceList.ListItems.Add(new TPH_PriceListJsonItem() { DestCityName = "Cancun", StayDuration = 7, LowestPrice = 1111 });
priceList.ListItems.Add(new TPH_PriceListJsonItem() { DestCityName = "Jamaica", StayDuration = 14, LowestPrice = 2222 });
// Serialize into XML string
DataContractSerializer serializer = new DataContractSerializer(priceList.GetType());
MemoryStream memStream = new MemoryStream();
serializer.WriteObject(memStream, priceList);
memStream.Seek(0, SeekOrigin.Begin);
string xmlOutput;
using (var streamReader = new StreamReader(memStream))
xmlOutput = streamReader.ReadToEnd();
// Serialize into JSON string
DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(priceList.GetType());
jsonSerializer.WriteObject(memStream = new MemoryStream(), priceList);
memStream.Seek(0, SeekOrigin.Begin);
string jsonOutput;
using (var streamReader = new StreamReader(memStream))
jsonOutput = streamReader.ReadToEnd();
}
}
public interface TPH_IPriceList
{
TPH_IPriceListItems ListItems { get; set; }
}
public interface TPH_IPriceListItems : IEnumerable<TPH_IPriceListItem>, IEnumerable, IList<TPH_IPriceListItem>
{
}
public interface TPH_IPriceListItem
{
string DestCityName { get; set; }
int StayDuration { get; set; }
int LowestPrice { get; set; }
}
[DataContract(Name = "PriceList")]
[KnownType(typeof(TPH_PriceListJsonItems))]
public class TPH_PriceListJson : TPH_IPriceList
{
[DataMember]
public TPH_IPriceListItems ListItems { get; set; }
public TPH_PriceListJson()
{
ListItems = new TPH_PriceListJsonItems();
}
}
[DataContract]
public class TPH_PriceListJsonItem : TPH_IPriceListItem
{
[DataMember(Order = 1)]
public string DestCityName { get; set; }
[DataMember(Order = 2)]
public int StayDuration { get; set; }
[DataMember(Order = 3)]
public int LowestPrice { get; set; }
public TPH_PriceListJsonItem()
{
}
}
[CollectionDataContract(Name = "ListItems", ItemName = "ListItem")]
[KnownType(typeof(TPH_PriceListJsonItem))]
public class TPH_PriceListJsonItems : List<TPH_IPriceListItem>, TPH_IPriceListItems, IEnumerable<TPH_IPriceListItem>, IEnumerable
{
public TPH_PriceListJsonItems(int capacity)
: base(capacity)
{
}
public TPH_PriceListJsonItems()
: base()
{
}
}
}
The inconsistency arises from a difference in how JSON and XML represent collections. For XML, the data contract serializers convert a collection to a nested set of elements -- an outer collection wrapper, and an inner element for each item in the collection. For JSON, the serializers convert a collection to an array containing objects. This seems reasonable, but there's a difference between the two: the XML outer element can have its own XML attributes, but JSON arrays cannot have their own properties -- there's simply no place for them in the standard.
This becomes an issue in dealing with type hints. Type hints are properties added to the serialized data to indicate, in the event of serializing an interface or base class of a class hierarchy, what concrete class was actually serialized. They are required to enable deserialization of the object without data loss. In XML, they appear as an i:type
attribute:
<PriceList xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Question32569055.V1">
<ListItems i:type="ListItems"> <!-- Notice the type hint here. -->
<ListItem i:type="TPH_PriceListJsonItem"> <!-- Notice the type hint here also. -->
<DestCityName>Cancun</DestCityName>
<StayDuration>7</StayDuration>
<LowestPrice>1111</LowestPrice>
</ListItem>
</ListItems>
</PriceList>
As you can see from your own example, type hints can be added for both collection and non-collection classes.
In JSON objects, they appear as an added property named "__type"
:
{
"__type": "TPH_PriceListJsonItem:#Question32569055.V3",
"DestCityName": "Cancun",
"StayDuration": 7,
"LowestPrice": 1111
}
But, as mentioned before, JSON arrays cannot have properties. So, what does DataContractJsonSerializer
do for polymorphic collection types? Well, other than for a few standard collection interfaces which, as Fabian notes, are mapped to collection classes using hardcoded logic, it throws a cryptic exception to indicate that subsequent deserialization would be impossible. (For comparison, Json.NET introduces an extra container object to hold collection type information. See TypeNameHandling setting.)
The solution to this inconsistency is to serialize the collection explicitly as a concrete collection (TPH_PriceListJsonItems
in your case) rather than as an interface:
[DataContract(Name = "PriceList")]
[KnownType(typeof(TPH_PriceListJsonItems))]
public class TPH_PriceListJson : TPH_IPriceList
{
[IgnoreDataMember]
public TPH_IPriceListItems ListItems
{
get
{
return ListItemList;
}
set
{
var list = value as TPH_PriceListJsonItems;
if (list == null)
{
list = new TPH_PriceListJsonItems();
if (value != null)
list.AddRange(value);
}
ListItemList = list;
}
}
[DataMember(Name = "ListItems")]
TPH_PriceListJsonItems ListItemList { get; set; }
public TPH_PriceListJson()
{
ListItemList = new TPH_PriceListJsonItems();
}
}
This eliminates the need for the type hint on the collection element while retaining it for collection members. It generates the following XML:
<PriceList xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Question32569055.V3">
<ListItems> <!-- No type hint here any more. -->
<ListItem i:type="TPH_PriceListJsonItem"> <!-- But the type hint is still here. -->
<DestCityName>Cancun</DestCityName>
<StayDuration>7</StayDuration>
<LowestPrice>1111</LowestPrice>
</ListItem>
</ListItems>
</PriceList>
And produces the following JSON:
{
"ListItems": [
{
"__type": "TPH_PriceListJsonItem:#Question32569055.V3",
"DestCityName": "Cancun",
"StayDuration": 7,
"LowestPrice": 1111
},
]
}
This design allows the class TPH_IPriceListItems
to control exactly what type of collection is used internally rather than leaving it up to users of the class, and thus seems like a preferable design overall.