I have a class with an internal list. I don't want the user of this class to be able to interact with the list directly, since I want to both keep it sorted and to run a calculation (which depends on the order) before returning it.
I expose
AddItem(Item x)
and a
IEnumerable<Item> Items
{
get { // returns a projection of internal list }
}
Serialization worked OK, but deserializing left the list empty. I figured it was because I didn't have a setter. So I added one that allowed you to set the list, but only if the internal list was empty. But this didn't solve the problem, turns out NewtonSoft does not call the setter, it only calls the getter to get the list, and then adds each item to it, which, since my getter returns a projected list, those items get added to an object that is immediately disposed once deserialization is done.
How do I maintain a read-only access to my list, while at the same time allowing for somewhat straightforward deserialization?
What worked for me was the following:
[JsonProperty(PropertyName = "TargetName")]
private List<SomeClass> _SomeClassList { get; set; }
public IReadOnlyList<SomeClass> SomeClassList
{
get
{
return this._SomeClassList.AsReadOnly();
}
}
Then, make a function to prevent SomeClassList to be serialized:
public bool ShouldSerializeSomeClassList() { return false; }
See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm (thanks Peska)
Looks like there's a number of ways to do it, but one thing I did not want to do was to have to modify all of my data objects to be aware of how they should be serialized/deserialized.
One way to do this was to take some examples of DefaultContractResolver's others had done (but still didn't do what I needed to do) and modify them to populate readonly fields.
Here's my class that I'd like to Serialize/Deserialize
public class CannotDeserializeThis
{
private readonly IList<User> _users = new List<User>();
public virtual IEnumerable<User> Users => _users.ToList().AsReadOnly();
public void AddUser(User user)
{
_users.Add(user);
}
}
I could serialize this to: {"Users":[{"Name":"First Guy"},{"Name":"Second Guy"},{"Name":"Third Guy"}]}
But Deserializing this would leave the Users IEnumerable empty. The only way, I could find, around this was to either remove the '.ToList.AsReadonly' on the Users property or implement a DefaultContractResolver as such:
public class ReadonlyJsonDefaultContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);
if (!prop.Writable)
{
var property = member as PropertyInfo;
if (property != null)
{
var hasPrivateSetter = property.GetSetMethod(true) != null;
prop.Writable = hasPrivateSetter;
if (!prop.Writable)
{
var privateField = member.DeclaringType.GetRuntimeFields().FirstOrDefault(x => x.Name.Equals("_" + Char.ToLowerInvariant(prop.PropertyName[0]) + prop.PropertyName.Substring(1)));
if (privateField != null)
{
var originalPropertyName = prop.PropertyName;
prop = base.CreateProperty(privateField, memberSerialization);
prop.Writable = true;
prop.PropertyName = originalPropertyName;
prop.UnderlyingName = originalPropertyName;
prop.Readable = true;
}
}
}
}
return prop;
}
}
The DefaultContractResolver is finding the corresponding private backing field, creating a property out of that, and renaming it to the public readonly property.
This assumes a convention, though. That your backing field starts with an underscore and is a lowercase version of your public property. For most of the code we were working with, this was a safe assumption. (e.g. 'Users' -> '_users', or 'AnotherPropertyName' -> '_anotherPropertyName')
With Newtonsoft
you can use a CustomCreationConverter<T>
or the abstract JsonConverter, you have to implement the Create
method and ReadJson
.
The ReadJson
method is where the converter will do the default deserialization calling the base method, from there, each item inside the readonly collection can be deserialized and added with the AddItem
method.
Any custom logic can be implemented inside AddItem.
The last step is configuring this new converter for deserialization with an attribute [JsonConverter(typeof(NavigationTreeJsonConverter))]
or within the JsonSerializerSettings
public class ItemsHolderJsonConverter : CustomCreationConverter<ItemsHolder>
{
public override bool CanConvert(Type objectType)
{
return typeof(ItemsHolder).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
JObject jObject = JObject.Load(reader);
ItemsHolder holder = base.ReadJson(CreateReaderFromToken(reader,jObject), objectType, existingValue, serializer) as ItemsHolder;
var jItems = jObject[nameof(ItemsHolder.Items)] as JArray ?? new JArray();
foreach (var jItem in jItems)
{
var childReader = CreateReaderFromToken(reader, jItem);
var item = serializer.Deserialize<Item>(childReader);
holder.AddItem(item);
}
return holder;
}
public override ItemsHolder Create(Type objectType)
{
return new ItemsHolder();
}
public static JsonReader CreateReaderFromToken(JsonReader reader, JToken token)
{
JsonReader jObjectReader = token.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateFormatString = reader.DateFormatString;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
jObjectReader.MaxDepth = reader.MaxDepth;
jObjectReader.SupportMultipleContent = reader.SupportMultipleContent;
return jObjectReader;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With