1

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
    };
}
MadSkunk
  • 3,309
  • 2
  • 32
  • 49
  • What's the difference, in business logic terms, between not supplying height and height being supplied as null? – Caius Jard Oct 13 '20 at 11:07
  • This is an example intermediate object I use to update some other data. Preferably I'd like to only set fields on the final object that were provided in the initial JSON. – MadSkunk Oct 13 '20 at 11:08
  • [How to configure JSON.net deserializer to track missing properties?](https://stackoverflow.com/q/30300740/3744182) looks to be a duplicate. Agree? – dbc Oct 13 '20 at 14:41
  • @dbc Agreed, thanks for the find! – MadSkunk Oct 16 '20 at 15:04

2 Answers2

0

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; }

}

svyat1s
  • 868
  • 9
  • 12
  • 21
0

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
}
MadSkunk
  • 3,309
  • 2
  • 32
  • 49