How to deserialize a read only List with Json.Net
Asked Answered
P

6

5

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?

Pegeen answered 9/12, 2014 at 19:20 Comment(3)
did you tried var list = new object[] { .... }; var ser = JsonConvert.SerializeObject(list, Formatting.Indented); ?Fukien
@Kiquenet: I'm not deserializing a list directly, I am deserializing an object that has a list as a read only property.Pegeen
@NeilN What worked for you?Tuyere
N
6

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)

Numbat answered 28/4, 2015 at 11:46 Comment(5)
I think this should be the accepted answer. Also, you can simply use [JsonIgnore] attribute on the public collection to prevent Json.NET to deserialize the ReadOnlyCollection.Syndactyl
This answer is confusing. What do you do with ShouldSerializeSomeClassList? How do you use it to prevent SomeClassList from bieng serialized?Oleum
@Oleum check documentation about Conditional Property Serialization: newtonsoft.com/json/help/html/ConditionalProperties.htmRy
@Ry No, the substantive details should be part of the answer is what I mean. The answer can be improved.Oleum
I have updated the answer to include the link, thank you.Numbat
A
4

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')

Aleman answered 28/11, 2018 at 16:50 Comment(0)
B
2

It seems that recent versions of Newtonsoft.Json automatically deserialize interface IReadOnlyList<T> to class ReadOnlyCollection<T>.

I'm using Newtonsoft.Json version 13.0.1 and this program works fine in Linqpad 7:

void Main()
{
    const String JSON = @"
    {
        ""propertyName"": [
            123,
            456
        ]
    }
    ";
    
    Dto dto = JsonConvert.DeserializeObject<Dto>( JSON );
    
    String x = "foo";
}

public class Dto
{
    [JsonConstructor]
    public Dto(
        [JsonProperty( "propertyName" )] IReadOnlyList<Int32> propertyName
    )
    {
        this.PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
    }
    
    public IReadOnlyList<Int32> PropertyName { get; }
}

Screenshot proof:

enter image description here

Blackfellow answered 13/8, 2022 at 18:11 Comment(0)
P
1

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;
    }
}
Postal answered 20/7, 2017 at 10:23 Comment(0)
C
0

I stumbled upon the answer on Stackoverflow in the comment section but, it wasn't voted. And, I give you more detailed answer here:

public class State
{
        [Newtonsoft.Json.JsonProperty]
        public double Citizens { get; private set; }

        [Newtonsoft.Json.JsonProperty]
        public float Value { get { return pValue; } }
        private float pValue = 450000.0f;

        public List<string> BeachList { get; } = new List<string>();

    public State()
    {
    }

    public State(double _Citizens)
    {
        this.Citizens = _Citizens;
    }
}

...

            State croatia = new State(30.0D);
            croatia.BeachList.Add("Bol na Braču");
            croatia.BeachList.Add("Zrće");

           string croatiaSerialized = Newtonsoft.Json.JsonConvert.SerializeObject(croatia);

           State slovenia = Newtonsoft.Json.JsonConvert.DeserializeObject<State>(croatiaSerialized);

So, croatia and, slovenia now both have the same property values. I added Citizens and, Value properties to see if you want to work with one or the other way.

Thanks to Saeb Amini (Private setters in Json.Net)

Cyclopentane answered 3/7, 2017 at 9:0 Comment(2)
My problem goes beyond just being able to set something via a private setter. While your solution will work for primitive types like float or double, it wont work for IEnumerables, like in my question. From above: "NewtonSoft does not call the setter, it only calls the getter to get the list, and then adds each item to it"Pegeen
You're right about that. I will get to bottom of this. I will get back here @NeilN.Cyclopentane
G
0

In my case, I just replaced the System.Text.Json library with Newtonsoft.Json and didn't need to change anything or create any converter classes.

Goto answered 18/5 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.