JSON Serialize Properties on Class Inheriting List

JSON serialize properties on class inheriting list

Your basic difficulty here is that JSON has two types of container: an object, and an array. From the standard:

  • An array is an ordered collection of values. An array begins with [ (left bracket) and ends with ] (right bracket). Values are separated by , (comma).

  • An object is an unordered set of name/value pairs. An object begins with { (left brace) and ends with } (right brace).

To force a collection's properties to be serialized, mark it with [JsonObject]:

[JsonObject]
public class TestResultListModel : List<TestResultModel>
{
public int TotalTestCases { get { return base.Count; } }

public int TotalSuccessful { get { return base.FindAll(t => t.Successful).Count; } }
}

Of course, if you do this, the items will not be serialized, because a JSON container can have properties, or items -- but not both. If you want both, you will need to add a synthetic array property to hold the items -- which can be private if you want.

[JsonObject] will also cause base class properties such as Capacity to be serialized, which you likely do not want. To suppress base class properties, use MemberSerialization.OptIn. Thus your final class should look something like:

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class TestResultListModel : List<TestResultModel>
{
[JsonProperty]
public int TotalTestCases { get { return base.Count; } }

[JsonProperty]
// Using Enumerable.Count() is more memory efficient than List.FindAll()
public int TotalSuccessful { get { return this.Count(t => t.Successful); } }

[JsonProperty]
TestResultModel[] Items
{
get
{
return this.ToArray();
}
set
{
if (value != null)
this.AddRange(value);
}
}
}

This gives JSON that looks like:

{
"TotalTestCases": 4,
"TotalSuccessful": 2,
"Items": [
{
"Successful": false,
"ErrorMessage": "STRING"
},
{
"Successful": true,
"ErrorMessage": "STRING"
},
{
"Successful": false,
"ErrorMessage": "STRING"
},
{
"Successful": true,
"ErrorMessage": "STRING"
}
]
}

It is possibly more work than it's worth, since these properties could be reconstructed easily on the client side. (The question Why not inherit from List? suggests avoiding this sort of design.)

Serialize object when the object inherits from list

According to the comments above (thanks!) there are two ways to get a correct result:

  • Implementing a custom JsonConverter (see here)
  • Workarround: Create a property in the class which returns the items (see here)

Anyway, inherit from List<T> is rare to be a good solution (see here)

I've tried it with the workarround:

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class A : List<B>
{
[JsonProperty]
public double TestA { get; set; }

[JsonProperty]
public B[] Items
{
get
{
return this.ToArray();
}
set
{
if (value != null)
this.AddRange(value);
}
}
}

public class B
{
public double TestB { get; set; }
}

This works for serialization and deserialization. Important: Items must be an Array of B and no List<B>. Otherwise deserialization doesn't work for Items.

JSON serialization with class inherited from ListT

As far as I know with Newtonsoft all you can do is something like this:

[JsonObject(MemberSerialization = MemberSerialization.Fields)]
public class DrivenList : List<int>
{

[JsonProperty]
public string Name { get; set; }

private DrivenList() { }

public DrivenList(string name) { this.Name = name; }
}

But this will add you unwanted (maybe) fields.

Personally I will do composition instead of inheritance:

public class DrivenList
{
public string Name { get; set; }

public List<int> Items { get; set; }

private DrivenList() { }

public DrivenList(string name) { this.Name = name; }
}

serialization of List of derived objects

From the documentation about javascriptserializer

For .NET Framework 4.7.2 and later versions, use the APIs in the System.Text.Json namespace for serialization and deserialization. For earlier versions of .NET Framework, use Newtonsoft.Json. This type was intended to provide serialization and deserialization functionality for AJAX-enabled applications

I.e. you should probably be using another json library.

If you prefer Json.Net, See Json.net serialize/deserialize derived types?. If you prefer System.Text.Json, see How to serialize properties of derived classes with System.Text.Json

.NET Core Serialize Inherited Class Properties -- Not Just Base Properties When Preserving References

The documentation on writing custom converters has a very similar example (discriminating between two subclasses of a property's declared type) and could be adapted as follows:

public class EmployeeConverter : JsonConverter<Employee>
{
enum TypeDiscriminator
{
Employee = 1,
Manager = 2
}

private static string s_typeDiscriminatorLabel = "$TypeDiscriminator";

public override bool CanConvert(Type typeToConvert) =>
typeof(Employee).IsAssignableFrom(typeToConvert);

public override Employee Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}

reader.Read();
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}

string propertyName = reader.GetString();
if (propertyName != s_typeDiscriminatorLabel)
{
throw new JsonException();
}

reader.Read();
if (reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}

// Instantiate type based on type discriminator value
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
Employee employee = typeDiscriminator switch
{
TypeDiscriminator.Employee => new Employee(),
TypeDiscriminator.Manager => new Manager(),
_ => throw new JsonException()
};

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return employee;
}

if (reader.TokenType == JsonTokenType.PropertyName)
{
propertyName = reader.GetString();
reader.Read();
switch (propertyName)
{
case "Name":
string name = reader.GetString();
employee.Name = name;
break;
case "Age":
int age = reader.GetInt32();
employee.Age = age;
break;
case "AllowedPersonalDays":
int allowedPersonalDays = reader.GetInt32();
if(employee is Manager manager)
{
manager.AllowedPersonalDays = allowedPersonalDays;
}
else
{
throw new JsonException();
}
break;
}
}
}

throw new JsonException();
}

public override void Write(
Utf8JsonWriter writer, Employee person, JsonSerializerOptions options)
{
writer.WriteStartObject();

// Write type indicator based on whether the runtime type is Manager
writer.WriteNumber(s_typeDiscriminatorLabel, (int)(person is Manager ? TypeDiscriminator.Manager : TypeDiscriminator.Employee));

writer.WriteString("Name", person.Name);
writer.WriteNumber("Age", person.Age);

// Write Manager-ony property only if runtime type is Manager
if(person is Manager manager)
{
writer.WriteNumber("AllowedPersonalDays", manager.AllowedPersonalDays);
}

writer.WriteEndObject();
}
}

Add an instance of your custom converter and it should deserialize correctly:

options.Converters.Add(new EmployeeConverter());

string serialized = JsonSerializer.Serialize<Store>(store, options);
var deserialized = JsonSerializer.Deserialize<Store>(serialized, options);
string reserialized = JsonSerializer.Serialize<Store>(deserialized, options);

System.Diagnostics.Debug.Assert(serialized == reserialized, "Manager property should be retained");

JsonConvert Class which is inheritance with a list

Sadly not every object can be serialized into valid json. And as I said in the comments what you want (something that has both a title property but is also a list) cannot be presented in valid json (in json something can have properties {} or something can be a list [] but not both), there are more structures that cannot be represented in valid json (circular dependencies for example) but sadly this is one of them.

Now I understand the need to inherit List<Group> for the ListView so this poses a problem. However this is not a problem that can't be worked around.

You need to separate the json from the models you bind to your views (this is in general a good practice imo). So you create an object that carries the data and can be serialized into valid json:

public class BodyGroupDto
{
public string Title;
public List<Body> Items;
}

This can now be represented by the following json:

[{
"Title": "SomeTitle",
"Items": [{
"Name": "Dimitris"
}]
}]

Now that we have the ability to send the data with json we still need to be able to convert our data object to the actual Group object we want to use in the view.

One way to do this is to add another constructor to the Group class so we can pass it both a title and items:

public class Group : List<Body>
{
public string Title { get; set; }
public Group(string title)
{
Title = title;
}

public Group(string title, IEnumerable<Body> items) : base(items)
{
Title = title;
}
}

And now we can convert this inside the Xamarin Forms application, for example that could look like this:

public List<Group> GetGroups(string json)
{
var bodyGroupList = JsonConvert.DeserializeObject<List<BodyGroupDto>>(json);

return bodyGroupList.Select(bodyGroup => new Group(bodyGroup.Title, bodyGroup.Items)).ToList();
}

And if you ever need to convert the other way around you could do something like this:

public string GetBodyGroupDtoListJson(List<Group> groups)
{
var bodyGroupList = groups.Select(group => new BodyGroupDto
{
Title = group.Title,
Items = group.ToList()
}).ToList();

return JsonConvert.SerializeObject(bodyGroupList);
}

Hope this helps and good luck with your project

Newtonsoft Serializes Hidden Inherited Members

Thanks to XavierXie-MSFT for pointing out that I could not tell the difference of which object was serializing because the data was the same in both objects. As I expected the original example was serializing the property from the base class. It wasn't an answer and only illustrated my point more clearly, but it did help me to analyze further.

In order to get the required behavior I had to make the base class Property2 virtual and override the property in the parent class. In this scenario Property2 still serialized but from the parent class. Then I changed to use the newtonsoft serialization ignore [JasonIgnore]. Then Property2 no longer serializes from either class when instantiated as Object2 type.

I would have expected hiding with new to have the same result.

I would have expected [IgnoreDataMember] to have ignored the overidden Property2 as [JsonIgnore] does.

I would have expected if there were no tag on Property2 in the parent class that the overridden Property2 would not have serialized.

Fortunately Virtualization and [JsonIgnore] do work.

solution:

[DataContract]
public class Object1
{
public Object1()
{
Property1 = "property 1 data from object 1";
Property2 = "property 2 data from object 1";
Property3 = "property 3 data from object 1";
Property0 = "should not be serialized from object 1";
}

public string Property0
{
get;
set;
}

[DataMember]
public string Property1
{
get;
set;
}

[DataMember]
public virtual string Property2
{
get;
set;
}

[DataMember]
public string Property3
{
get;
set;
}

}

[DataContract]
public class Object2 : Object1
{
public Object2()
{
Property4 = "property 4 data from object 2";
Property5 = "property 5 data from object 2";
Property6 = "property 6 data from object 2";
Property2 = "should not be serialized from object 2";
Property7 = "should not be serialized from object 2";
}

[DataMember]
public string Property4
{
get;
set;
}

[DataMember]
public string Property5
{
get;
set;
}

[DataMember]
public string Property6
{
get;
set;
}

/// <summary>
/// from parent class
/// </summary>
[JsonIgnore]
public override string Property2
{
get;
set;
}

public string Property7
{
get;
set;
}

}

Object2 myobject = new Object2();

string serialized = JsonConvert.SerializeObject(myobject);

Object1 mybaseobject = new Object1();

string baseserialized = JsonConvert.SerializeObject(mybaseobject);

Results:

serialized
{"Property4":"property 4 data from object 2","Property5":"property 5 data from object 2","Property6":"property 6 data from object 2","Property1":"property 1 data from object 1","Property3":"property 3 data from object 1"}

baseserialized
{"Property1":"property 1 data from object 1","Property2":"property 2 data from object 1","Property3":"property 3 data from object 1"}

Note: The C# UWP DataContractSerializer will still serialize Property2 from the parent class even when using [IgnoreDataMember].

Serialize a list of derived list

You don't want to inherit from List<T>.

Create a list property instead:

public class GroupeActes
{
public List<Acte> Actes { get; set; }

public string NomGroupe { get; set; }

public GroupeActes(string nom, List<Acte> liste)
{
NomGroupe = nom;

Actes.AddRange(acte);
}
}

Lists (and other collection types) get special treatment while serializing. You don't want the collection type's public properties (such as Capacity and Count) in your output, so the property you added through inheritance won't be serialized either.

A collection is serialized like this:

if o is IEnumerable
foreach object s in o
serialize o

So the serializer won't even look at your enumerable's properties.



Related Topics



Leave a reply



Submit