2
votes

I noticed that, by default, JSON.NET will only (de-)serialize an object's public properties. This is good. However, when a property is tagged with a [JsonPropertyAttribute], JSON.NET will also access private getters and setters. This is bad.

What I'd like to do to fix this problem is mark the private getter/setter with a [JsonIgnoreAttribute].

For example:

public class JsonObject
{
    [JsonProperty(PropertyName = "read_write_property")]
    public object ReadOnlyProperty
    {
        get;
        [JsonIgnore] private set;
    }
}

Unfortunately, that's not valid C# code. So what code would achieve the same result?

Some of the ideas that I know would work:

  1. Remove the [JsonPropertyAttribute]
  2. Remove the setter entirely and introduce a backing field

Are these the only two options?

Edit

I added the backing field for my read-only property. Not sure, but I think I found a bug in Json.Net. The serializer will treat the property as non-existent when only a getter exists, even though the specified name attribute matches the JSON string. This is especially annoying, because I'm also using the [JsonExtensionData] mechanism. So the deserialized value ends up going into my extension data dictionary. Here is the code that demonstrates the problem:

Offending class

using System.ComponentModel;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

public class JsonObject
{
    private readonly BindingDirection bindingDirection;

    public JsonObject()
    {
        this.bindingDirection = BindingDirection.OneWay;
    }

    [JsonProperty(PropertyName = "binding_type"), JsonConverter(typeof(StringEnumConverter))]
    public BindingDirection BindingDirection
    {
        get
        {
            return this.bindingDirection;
        }
    }

    [JsonExtensionData]
    public IDictionary<string, object> ExtensionData { get; set; }
}

Demonstration

using Newtonsoft.Json;

class Program
{
    static void Main(string[] args)
    {
        var obj = new JsonObject();

        var serialized = JsonConvert.SerializeObject(obj);

        var deserialized = JsonConvert.DeserializeObject<JsonObject>(serialized);

        Console.WriteLine("*** Extension data ***\n");

        foreach (var kvp in deserialized.ExtensionData)
        {
            Console.WriteLine("{0} == {1}", kvp.Key, kvp.Value);
        }

        Console.ReadLine();
    }
}

Output

*** Extension data ***

binding_type == OneWay

1
What result are you actually trying to achieve? If you have a public field with a private setter, are you saying that you want that field serialized but not deserialized? - Brian Rogers
@BrianRogers, yep. I only want that property to be serialized to JSON. The property is initialized from the constructor (not shown in the example), so I don't have to ever retrieve it from the JSON string. - Steven Liekens
But most of all, I just want JSON.NET to stop violating one of the most fundamental principles of object oriented programming! - Steven Liekens

1 Answers

4
votes

The fact that Json.Net accesses private members when tagged with the [JsonProperty] attribute is actually by design. This feature exists to enable Json.Net to be used for serializing/deserializing the internal state of an object (e.g. to persist it) without requiring exposing a public interface for that state.
From the documentation:

JsonPropertyAttribute

JsonPropertyAttribute has a number of uses:

  • By default the JSON property will have the same name as the .NET property. This attribute allows the name to be customized.

  • Indicates that a property should be serialized when member serialization is set to opt-in.

  • Includes non-public properties in serialization and deserialization.

  • Customize type name, reference, null and default value handling for the property value.

  • Customize the property's collection items JsonConverter, type name handing and reference handling.

Unfortunately, the parameters of the [JsonProperty] attribute apparently do not provide a way to opt out of including non-public members while still allowing you to use other features such as changing the property name. Therefore, you will have to use a workaround in this situation.

You already mentioned a couple of possibilities, to which I will add a third:

  1. Remove the [JsonPropertyAttribute]
  2. Remove the setter entirely and introduce a backing field
  3. Implement a custom JsonConverter for your class

Of these, the second one is the cleanest and easiest to implement while still achieving the result you are looking for: the public property will be serialized using the customized property name, while the private backing field will not be affected during deserialization.

The first option, removing the attribute, won't quite get you what you want. While it will prevent the field from being deserialized into, you will have to settle for the original property name being written to the JSON during serialization.

The third option, writing a converter, gives you complete control over how your class is serialized and deserialized. You can change the property names, omit properties, include additional information that isn't even in the class, whatever. They are not that difficult to write; however, if all you really need is a simple backing field to solve your problem, a converter is probably overkill here. That said, if you're really interested in this option, I'd be happy to provide a simple example. Just let me know.

Edit

Regarding your Extension Data problem-- you're right, that does look like a bug. When I originally tried to reproduce the problem, I could not, because my version of your JsonObject did not contain a default constructor. I was assuming your constructor had a parameter to accept the initial value of the read-only property. Once I removed the parameter, it started misbehaving. I'm not sure why the constructor would make a difference in how the JsonExtensionData is interpretted. However, this does suggest an odd workaround: try adding the following to your class and see if that fixes the problem.

    [JsonConstructor]
    private JsonObject(string dummy) : this()
    {
    }