GreenEyedAndy
GreenEyedAndy

Reputation: 1525

can't deserialize collection of generic types

Json.net doesn't deserialize JSON it previously created, if there is a collection of generic types. How can I deserialize such JSON correctly?

Here is the JSON I am trying to deserialize:

{  
    "$type":"MyProject.Messages.ChangeMsg`1[[MyProject.Classes.DTO.DeviceDTO, MyProject]], MyProject",
    "ChangedDataList":{  
        "$type":"System.Collections.Generic.List`1[[System.Tuple`2[[MyProject.Classes.DTO.DeviceDTO, MyProject],[MyProject.Enums.ChangedStatus, MyProject]], mscorlib]], mscorlib",
        "$values":[  
            {  
                "$type":"System.Tuple`2[[MyProject.Classes.DTO.DeviceDTO, MyProject],[MyProject.Enums.ChangedStatus, MyProject]], mscorlib",
                "Item1":{  
                    "$type":"MyProject.Classes.DTO.SwitchDeviceDTO, MyProject",
                    "Id":318,
                    "Name":"Device",
                    "Ios":{  
                        "$type":"System.Collections.Generic.List`1[[MyProject.Classes.DTO.IoDTO, MyProject]], mscorlib",
                        "$values":[  

                        ]
                    },
                    "GuiProperties":{  
                        "$type":"System.Collections.Generic.List`1[[MyProject.Classes.DTO.GuiPropertiesDTO, MyProject]], mscorlib",
                        "$values":[  
                            {  
                                "$type":"MyProject.Classes.DTO.GuiPropertiesDTO, MyProject",
                                "Id":319,
                                "X":200,
                                "Y":0,
                                "DeviceId":318,
                                "ChangedStatus":0
                            }
                        ]
                    },
                    "ChangedStatus":0
                },
                "Item2":0
            }
        ]
    }
}

I can't see any error in the output-window, but after deserialization ChangedDataList is always null.

Here the deserialization code:

private static T GetMessage<T>(string msg)
{
    JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
    return JsonConvert.DeserializeObject<T>(msg, settings);
}

And here is a full LinqPad Example:

void Main()
{
    ChangeMsg<DeviceDTO> chgMsg = new ChangeMsg<DeviceDTO>(new List<Tuple<DeviceDTO, ChangedStatus>>() { new Tuple<DeviceDTO, ChangedStatus>(new DeviceDTO() { Id = 318, Name = "Device" }, ChangedStatus.New)});

    var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
    string msg = JsonConvert.SerializeObject(chgMsg, settings);

    var obj = JsonConvert.DeserializeObject(msg, settings);
}

// Define other methods and classes here
public enum ChangedStatus
{
    New,
    Modified,
    Deleted
}

[DataContract]
public enum ChangedStatusDTO
{
    [EnumMember]
    New,
    [EnumMember]
    Modified,
    [EnumMember]
    Deleted
}

public interface IChangedStatusDTO
{
    ChangedStatusDTO ChangedStatus { get; set; }
}

public abstract class SdnMessage
{
    public int RequestId { get; set; }
}

[JsonObject(MemberSerialization.OptIn)]
public class ChangeMsg<T> : SdnMessage where T : IChangedStatusDTO
{
    [JsonConstructor]
    public ChangeMsg(List<Tuple<T, ChangedStatus>> tuples)
    {
        ChangedDataList = tuples;
    }

    [JsonProperty("ChangedDataList")]
    public List<Tuple<T, ChangedStatus>> ChangedDataList { get; }
}

[DataContract(IsReference = true)]
public class ConnectionDTO : IChangedStatusDTO
{
    [DataMember]
    public int Id { get; set; }
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public int FromId { get; set; }

    [DataMember]
    public int? FromDeviceId { get; set; }

    [DataMember]
    public int ToId { get; set; }

    [DataMember]
    public int? ToDeviceId { get; set; }

    [DataMember]
    public ChangedStatusDTO ChangedStatus { get; set; }
}

[DataContract]
public class GuiPropertiesDTO : IChangedStatusDTO
{
    [DataMember]
    public int Id { get; set; }
    [DataMember]
    public int X { get; set; }
    [DataMember]
    public int Y { get; set; }
    [DataMember]
    public int Z { get; set; }
    [DataMember]
    public int Width { get; set; }
    [DataMember]
    public int Height { get; set; }
    [DataMember]
    public int? DeviceId { get; set; }

    [DataMember]
    public ChangedStatusDTO ChangedStatus { get; set; }
}

[DataContract(IsReference = true)]
public class IoDTO : IChangedStatusDTO
{
    [DataMember]
    public int Id { get; set; }
    [DataMember]
    public int Number { get; set; }
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public string Description { get; set; }
    [DataMember]
    public int? DeviceId { get; set; }
    [DataMember]
    public virtual ICollection<ConnectionDTO> ConnectionFroms { get; set; }
    [DataMember]
    public virtual ICollection<ConnectionDTO> ConnectionTos { get; set; }
    [DataMember]
    public ChangedStatusDTO ChangedStatus { get; set; }
}

[DataContract(IsReference = true)]
public class DeviceDTO : IChangedStatusDTO
{
    public DeviceDTO()
    {
        Name = string.Empty;
        Ios = new HashSet<IoDTO>();
        GuiProperties = new HashSet<GuiPropertiesDTO>();
    }
    [DataMember]
    public int Id { get; set; }
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public int ManufacturerId { get; set; }
    [DataMember]
    public virtual ICollection<IoDTO> Ios { get; set; }
    [DataMember]
    public virtual ICollection<GuiPropertiesDTO> GuiProperties { get; set; }

    [DataMember]
    public ChangedStatusDTO ChangedStatus { get; set; }
}

Upvotes: 0

Views: 286

Answers (1)

dbc
dbc

Reputation: 116991

Your problem is with the ChangeMsg<T> constructor:

[JsonObject(MemberSerialization.OptIn)]
public class ChangeMsg<T> : SdnMessage where T : IChangedStatusDTO
{
    [JsonConstructor]
    public ChangeMsg(List<Tuple<T, ChangedStatus>> tuples)
    {
        ChangedDataList = tuples;
    }

    [JsonProperty("ChangedDataList")]
    public List<Tuple<T, ChangedStatus>> ChangedDataList { get; }
}

The name of the constructor's argument is tuples, which is not the same as the property name "ChangedDataList". Thus when deserializing JSON with a "ChangedDataList" property as shown in the question, Json.NET has no way to know that it should be bound to the tuples argument. This is because Json.NET will bind JSON properties to constructor arguments by matching their names, modulo case. Instead null is passed in and the ChangedDataList c# property is never allocated. As this property is get-only, Json.NET is subsequently unable to populate it, and the values from the JSON are skipped.

To solve the problem, you can change the name of the constructor argument to be consistent with the property name. You should also either allocate an empty list or throw a ArgumentNullException if null is passed in:

[JsonConstructor]
public ChangeMsg(List<Tuple<T, ChangedStatus>> changedDataList)
{
    this.ChangedDataList = changedDataList ?? new List<Tuple<T, ChangedStatus>>();
}

(My preference is not to throw ArgumentNullException from deserialization code, but your preferences may differ.)

Alternatively, if you are think it is too fragile that serialization depends on the naming of constructor arguments, you can explicitly mark the constructor argument with [JsonProperty] like so:

[JsonConstructor]
public ChangeMsg( [JsonProperty("ChangedDataList")] List<Tuple<T, ChangedStatus>> tuples)
{
    this.ChangedDataList = tuples ?? new List<Tuple<T, ChangedStatus>>();
}

Upvotes: 1

Related Questions