Is it possible to determine which properties were deserialised in Json.Net?

281 views Asked by At

Suppose I have the following JSON:

{
  "name": "Jim",
  "age": 20
}

And I deserialise it into the following C# object:

public class Person
{
  [JsonProperty("name")]
  public string Name { get; set; }

  [JsonProperty("age")]
  public int? Age    { get; set; }

  [JsonProperty("height")]
  public int? Height { get; set; }
}

Is there any way I can determine which properties were included in the the original JSON, and which were omitted?

In this example all my properties are nullable, the JSON didn't include the height property, so my C# object will have end up with a null Height.

However it's also possible that a user could simply provide null as the height, e.g.

{
  "name": "Jim",
  "age": 20,
  "height": null
}

So my question is: Is it possible for me to determine if the value was provided but null, or not provided and therefore defaulting to null. Is there some meta data available somewhere/somehow that gives me this information?

This is used in an ApiController, so the deserialization is done by a Formatter automatically, but here is my current formatter setup:

private static void AddFormatter(HttpConfiguration config)
{
    var formatter = config.Formatters.JsonFormatter;

    formatter.SerializerSettings = new JsonSerializerSettings
    {
        Formatting       = Formatting.Indented,
        TypeNameHandling = TypeNameHandling.None
    };
}
2

There are 2 answers

0
MadSkunk On

Due to the lack of response, barring the helpful but not brilliant suggestion by svyatis.lviv I decided to implement it in a simple non serialisation related way.

First I made this inteface:

public interface IPropertyChangeLog
{
    IEnumerable<string> PropertiesChanged { get; }
    void Reset();
}

Then I made this helper class:

public class PropertyChangeLog<TSource> : IPropertyChangeLog
    where TSource : IPropertyChangeLog
{
    private readonly List<string> _changes = new List<string>();

    public void UpdateProperty<TValue>(TValue newValue, ref TValue oldValue, [CallerMemberName] string propertyName = null)
    {
        oldValue = newValue;
        _changes.Add(propertyName);
    }

    public IEnumerable<string> PropertiesChanged => _changes;
    public void Reset() => _changes.Clear();
}

And finally, I updated the Person class as follows:

public class Person : IPropertyChangeLog
{
    private PropertyChangeLog<Person> _log = new PropertyChangeLog<Person>();
    private string _name;
    private int? _age;
    private int? _height;

    [JsonProperty("name")]
    public string Name
    {
        get => _name;
        set => _log.UpdateProperty(value, ref _name);
    }

    [JsonProperty("age")]
    public int? Age
    {
        get => _age;
        set => _log.UpdateProperty(value, ref _age);
    }

    [JsonProperty("height")]
    public int? Height
    {
        get => _height;
        set => _log.UpdateProperty(value, ref _height);
    }

    IEnumerable<string> IPropertyChangeLog.PropertiesChanged => _log.PropertiesChanged;
    void IPropertyChangeLog.Reset() => _log.Reset();
}

It's a little more verbose than I'd like, but it's still pretty simple and readable.

In order to use it:

var person = JsonConvert.DeserializeObject<Person>("{ \"name\": \"test\" }");
var log = (IPropertyChangeLog)person;

// log.PropertiesChanged should now contain 'Name'

foreach (var property in log.PropertiesChanged)
{
    // we know that the property named in 'property' changed
}

log.Reset();

JsonConvert.PopulateObject("{ \"age\": null }", person);

// now log.PropertiesChanged should only contain 'Age'

foreach (var property in log.PropertiesChanged)
{
    // we know that the property named in 'property' changed
}
2
svyat1s On

You can use DefaultValueHandling attribute to define the strategy for handling nullable values like [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)], you can read more options here.

try adding the default value that user can't provide and then you'd know if the value was user provided or not.

public class Person
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public int? Age    { get; set; }

    [DefaultValue(-1)] 
    [JsonProperty("height", DefaultValueHandling = DefaultValueHandling.Populate)]
    public int? Height { get; set; }

}