Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to configure JSON.net deserializer to track missing properties?

Tags:

c#

json.net

Sample class:

public class ClassA
{
    public int Id { get; set; }
    public string SomeString { get; set; }
    public int? SomeInt { get; set; }
}

Default deserializer:

var myObject = JsonConvert.DeserializeObject<ClassA>(str);

Create the same object for two different inputs

{"Id":5}

or

{"Id":5,"SomeString":null,"SomeInt":null} 

How can I track properties that were missing during deserialization process and preserve the same behavior? Is there a way to override some of JSON.net serializer methods (e.g. DefaultContractResolver class methods) to achive this. For example:

List<string> missingProps;
var myObject = JsonConvert.DeserializeObject<ClassA>(str, settings, missingProps);

For the first input list should contains the names of the missing properties ("SomeString", "SomeInt") and for second input it should be empty. Deserialized object remains the same.

like image 423
Milos Avatar asked May 18 '15 10:05

Milos


3 Answers

1. JSON has a property which is missing in your class

Using property JsonSerializerSettings.MissingMemberHandling you can say whether missing properties are handled as errors.

Than you can install the Error delegate which will register errors.

This will detect if there is some "garbage" property in JSON string.

public class ClassA
{
    public int Id { get; set; }
    public string SomeString { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        const string str = "{'Id':5, 'FooBar': 42 }";
        var myObject = JsonConvert.DeserializeObject<ClassA>(str
            , new JsonSerializerSettings
            {
                Error = OnError,
                MissingMemberHandling = MissingMemberHandling.Error
            });

        Console.ReadKey();
    }

    private static void OnError(object sender, ErrorEventArgs args)
    {
        Console.WriteLine(args.ErrorContext.Error.Message);
        args.ErrorContext.Handled = true;
    }
}

2. Your class has a property which is missing in JSON

Option 1:

Make it a required property:

    public class ClassB
    {
        public int Id { get; set; }

        [JsonProperty(Required = Required.Always)]
        public string SomeString { get; set; }

    }
Option 2:

Use some "special" value as a default value and check afterwards.

public class ClassB
{
    public int Id { get; set; }

    [DefaultValue("NOTSET")]
    public string SomeString { get; set; }
    public int? SomeInt { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        const string str = "{ 'Id':5 }";
        var myObject = JsonConvert.DeserializeObject<ClassB>(str
            , new JsonSerializerSettings
            {
                DefaultValueHandling = DefaultValueHandling.Populate
            });

        if (myObject.SomeString == "NOTSET")
        {
            Console.WriteLine("no value provided for property SomeString");
        }

        Console.ReadKey();
    }
}
Option 3:

Another good idea would be to encapsulate this check iside the class istself. Create a Verify() method as shown below and call it after deserialization.

public class ClassC
{
    public int Id { get; set; }

    [DefaultValue("NOTSET")]
    public string SomeString { get; set; }
    public int? SomeInt { get; set; }

    public void Verify()
    {
        if (SomeInt == null ) throw new JsonSerializationException("SomeInt not set!");
        if (SomeString == "NOTSET") throw new JsonSerializationException("SomeString not set!");
    }
}
like image 79
George Mamaladze Avatar answered Oct 29 '22 22:10

George Mamaladze


Another way to find null/undefined tokens during De-serialization is to write a custom JsonConverter , Here is an example of custom converter which can report both omitted tokens (e.g. "{ 'Id':5 }") and null tokens (e.g {"Id":5,"SomeString":null,"SomeInt":null})

public class NullReportConverter : JsonConverter
{
    private readonly List<PropertyInfo> _nullproperties=new List<PropertyInfo>();
    public bool ReportDefinedNullTokens { get; set; }

    public IEnumerable<PropertyInfo> NullProperties
    {
        get { return _nullproperties; }
    }

    public void Clear()
    {
        _nullproperties.Clear();
    }

    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        existingValue = existingValue ?? Activator.CreateInstance(objectType, true);

        var jObject = JObject.Load(reader);
        var properties =
            objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        foreach (var property in properties)
        {
            var jToken = jObject[property.Name];
            if (jToken == null)
            {
                _nullproperties.Add(property);
                continue;
            }

            var value = jToken.ToObject(property.PropertyType);
            if(ReportDefinedNullTokens && value ==null)
                _nullproperties.Add(property);

            property.SetValue(existingValue, value, null);
        }

        return existingValue;
    }

    //NOTE: we can omit writer part if we only want to use the converter for deserializing
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var objectType = value.GetType();
        var properties =
            objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        writer.WriteStartObject();
        foreach (var property in properties)
        {
            var propertyValue = property.GetValue(value, null);
            writer.WritePropertyName(property.Name);
            serializer.Serialize(writer, propertyValue);
        }

        writer.WriteEndObject();
    }
}

Note: we can omit the Writer part if we don't need to use it for serializing objects.

Usage Example:

class Foo
{
    public int Id { get; set; }
    public string SomeString { get; set; }
    public int? SomeInt { get; set; }
}

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

        Console.WriteLine("Pass 1");
        var obj0 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5, \"Id\":5}", nullConverter);
        foreach(var p in nullConverter.NullProperties)
            Console.WriteLine(p);

        nullConverter.Clear();

        Console.WriteLine("Pass2");
        var obj1 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5,\"SomeString\":null,\"SomeInt\":null}" , nullConverter);
        foreach (var p in nullConverter.NullProperties)
            Console.WriteLine(p);

        nullConverter.Clear();

        nullConverter.ReportDefinedNullTokens = true;
        Console.WriteLine("Pass3");
        var obj2 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5,\"SomeString\":null,\"SomeInt\":null}", nullConverter);
        foreach (var p in nullConverter.NullProperties)
            Console.WriteLine(p);
    }
}
like image 31
user3473830 Avatar answered Oct 29 '22 21:10

user3473830


I got this problem, but defaultValue was not solution due to POCO object. I think this is simpler approach than NullReportConverter. There are three unit tests. Root is class that encapsulate whole json. Key is type of the Property. Hope this helps someone.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;

namespace SomeNamespace {
  [TestClass]
  public class NullParseJsonTest {
    [TestMethod]
    public void TestMethod1()
    {
      string slice = "{Key:{guid:\"asdf\"}}";
      var result = JsonConvert.DeserializeObject<Root>(slice);

      Assert.IsTrue(result.OptionalKey.IsSet);
      Assert.IsNotNull(result.OptionalKey.Value);
      Assert.AreEqual("asdf", result.OptionalKey.Value.Guid);
    }

    [TestMethod]
    public void TestMethod2()
    {
      string slice = "{Key:null}";
      var result = JsonConvert.DeserializeObject<Root>(slice);

      Assert.IsTrue(result.OptionalKey.IsSet);
      Assert.IsNull(result.OptionalKey.Value);
    }

    [TestMethod]
    public void TestMethod3()
    {
      string slice = "{}";
      var result = JsonConvert.DeserializeObject<Root>(slice);

      Assert.IsFalse(result.OptionalKey.IsSet);
      Assert.IsNull(result.OptionalKey.Value);
    }
  }

  class Root {

    public Key Key {
      get {
        return OptionalKey.Value;
      }
      set {
        OptionalKey.Value = value;
        OptionalKey.IsSet = true;   // This does the trick, it is never called by JSON.NET if attribute missing
      }
    }

    [JsonIgnore]
    public Optional<Key> OptionalKey = new Optional<Key> { IsSet = false };
  };


  class Key {
    public string Guid { get; set; }
  }

  class Optional<T> {
    public T Value { get; set; }
    public bool IsSet { get; set; }
  }
}
like image 42
Peter Dub Avatar answered Oct 29 '22 23:10

Peter Dub