I am trying to serialize model graph using newtonsoft.json, and I know that using ignore attribute or using custom ContractResolver I can ignore specific properties from being serialized?
Here's the custom ContractResolver I am using to ignore properties:
public class PropertyIgnoreSerializerContractResolver : DefaultContractResolver
{
private readonly Dictionary<Type, HashSet<string>> _ignores;
public PropertyIgnoreSerializerContractResolver()
{
_ignores = new Dictionary<Type, HashSet<string>>();
}
public void IgnoreProperty(Type type, params string[] jsonPropertyNames)
{
if (!_ignores.ContainsKey(type))
_ignores[type] = new HashSet<string>();
foreach (var prop in jsonPropertyNames)
_ignores[type].Add(prop);
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (IsIgnored(property.DeclaringType, property.PropertyName))
property.ShouldSerialize = i => false;
return property;
}
private bool IsIgnored(Type type, string jsonPropertyName)
{
if (!_ignores.ContainsKey(type))
return false;
return _ignores[type].Contains(jsonPropertyName);
}
}
But in my case I need to include specific properties inside the graph model to be serialized rather than exclude a lot of properties inside that graph?
Is there any way to configure it to serialize specific properties?
I have modified PropertyIgnoreSerializerContractResolver a little bit to reverse the logic to include properties rather than ignoring them.
First of all imagine we have these classes:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public List<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public Person Teacher { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime? BOD { get; set; }
public decimal Salary { get; set; }
}
Serializing class and my custom ContractResolver to include properties to be serialized they look like this:
public static class SerilizationExtensions
{
public static IContractResolver contractResolver { get; set; }
public static string ToJson(this object obj, IContractResolver contractResolver=null)
{
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings()
{
PreserveReferencesHandling = PreserveReferencesHandling.All,
ContractResolver =contractResolver==null? new PropertyIgnoreSerializerContractResolver():contractResolver
});
}
}
public class PropertyIncludeSerializerContractResolver : DefaultContractResolver
{
private readonly Dictionary<Type, HashSet<string>> _includedProperties;
public PropertyIncludeSerializerContractResolver()
{
_includedProperties = new Dictionary<Type, HashSet<string>>();
}
public void IncludeProperty(Type type, params string[] jsonPropertyNames)
{
if (!_includedProperties.ContainsKey(type))
_includedProperties[type] = new HashSet<string>();
foreach (var prop in jsonPropertyNames)
_includedProperties[type].Add(prop);
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (IsIncluded(property.DeclaringType, property.PropertyName) || property.PropertyType.IsValueType || property.PropertyType==typeof(string))
property.ShouldSerialize = i => true;
else
property.ShouldSerialize = i => false;
return property;
}
private bool IsIncluded(Type type, string jsonPropertyName)
{
if (!_includedProperties.ContainsKey(type))
return false;
return _includedProperties[type].Contains(jsonPropertyName);
}
}
Inside Main method :
static void Main(string[] args)
{
var student = new Student
{
Id=1,
Name="Simple Code",
Courses=new List<Course> {
new Course{ Id=1, Name="history", Teacher=new Person{Id=1,Name="James",BOD=DateTime.UtcNow,Salary=1000.50M } },
new Course{ Id=2, Name="Math", Teacher=new Person{Id=2,Name="David",BOD=DateTime.UtcNow,Salary=6000.50M } }
}
};
var jsonResolver = new PropertyIncludeSerializerContractResolver();
jsonResolver.IncludeProperty(typeof(Student), "Courses");
// if you want Teacher property to get serialized uncomment this code
//jsonResolver.IncludeProperty(typeof(Course), "Teacher");
var jsonStr = student.ToJson(jsonResolver);
Console.WriteLine(jsonStr);
Console.ReadLine();
}
So here PropertyIncludeSerializerContractResolver will ignore all reference types which are not specified to be serialized.
I hope my code will help others.
Related
Some of my actions accept models like:
public class PaymentRequest
{
public decimal Amount { get; set; }
public bool? SaveCard { get; set; }
public int? SmsCode { get; set; }
public BankCardDetails Card { get; set; }
}
public class BankCardDetails
{
public string Number { get; set; }
public string HolderName { get; set; }
public string ExpiryDate { get; set; }
public string ValidationCode { get; set; }
}
And the action method looks like:
[HttpPost]
[Route("api/v1/payment/pay")]
public Task<BankCardActionResponse> Pay([FromBody] PaymentRequest request)
{
if (request == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
return _paymentService.PayAsync(DataUserHelper.PhoneNumber, request);
}
I use Nlog. I think it's clear this is a bad idea to log all this bank data. My log config file contained the following line:
<attribute name="user-requestBody" layout="${aspnet-request-posted-body}"/>
I logged the request. I decided to refactor that and planned the following strategy. Actions that contain sensitive data into their requests I will mark with an attribute like
[RequestMethodFormatter(typeof(PaymentRequest))]
then take a look at my custom renderer:
[LayoutRenderer("http-request")]
public class NLogHttpRequestLayoutRenderer : AspNetRequestPostedBody
{
protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
{
base.DoAppend(builder, logEvent);
var body = builder.ToString();
// Get attribute of the called action.
var type = ... // How can I get "PaymentRequest" from the [RequestMethodFormatter(typeof(PaymentRequest))]
var res = MaskHelper.GetMaskedJsonString(body, type);
// ... and so on
}
}
I think you understand the idea. I need the type from the method's RequestMethodFormatter attribute. Is it even possible to get it into the renderer? I need it because I'm going to deserialize request JSON into particular models (it's gonna be into the MaskHelper.GetMaskedJsonString), work with the models masking the data, serialize it back into JSON.
So, did I choose a wrong approach? Or it's possible to get the type from the attribute into the renderer?
After some research, I ended up with the following solution:
namespace ConsoleApp7
{
internal class Program
{
private static void Main()
{
var sourceJson = GetSourceJson();
var userInfo = JsonConvert.DeserializeObject(sourceJson, typeof(User));
Console.WriteLine("----- Serialize without Resolver-----");
Console.WriteLine(JsonConvert.SerializeObject(userInfo));
Console.WriteLine("----- Serialize with Resolver-----");
Console.WriteLine(JsonConvert.SerializeObject(userInfo, new JsonSerializerSettings
{
ContractResolver = new MaskPropertyResolver()
}));
}
private static string GetSourceJson()
{
var guid = Guid.Parse("3e92f0c4-55dc-474b-ae21-8b3dac1a0942");
return JsonConvert.SerializeObject(new User
{
UserId = guid,
Age = 19,
Name = "John",
BirthDate = new DateTime(1990, 5, 12),
Hobbies = new[]
{
new Hobby
{
Name = "Football",
Rating = 5,
DurationYears = 3,
},
new Hobby
{
Name = "Basketball",
Rating = 7,
DurationYears = 4,
}
}
});
}
}
public class User
{
[MaskGuidValue]
public Guid UserId { get; set; }
[MaskStringValue("***")] public string Name { get; set; }
public int Age { get; set; }
[MaskDateTimeValue]
public DateTime BirthDate { get; set; }
public Hobby[] Hobbies { get; set; }
}
public class Hobby
{
[MaskStringValue("----")]
public string Name { get; set; }
[MaskIntValue(replacement: 11111)]
public int Rating { get; set; }
public int DurationYears { get; set; }
}
public class MaskPropertyResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var props = base.CreateProperties(type, memberSerialization);
var allowedPropertyTypes = new Type[]
{
typeof(Guid),
typeof(DateTime),
typeof(string),
typeof(int),
};
foreach (var prop in props.Where(p => allowedPropertyTypes.Contains(p.PropertyType)))
{
if (prop.UnderlyingName == null)
continue;
var propertyInfo = type.GetProperty(prop.UnderlyingName);
var attribute =
propertyInfo?.GetCustomAttributes().FirstOrDefault(x => x is IMaskAttribute) as IMaskAttribute;
if (attribute == null)
{
continue;
}
if (attribute.Type != propertyInfo.PropertyType)
{
// Log this case, cause somebody used wrong attribute
continue;
}
prop.ValueProvider = new MaskValueProvider(propertyInfo, attribute.Replacement, attribute.Type);
}
return props;
}
private class MaskValueProvider : IValueProvider
{
private readonly PropertyInfo _targetProperty;
private readonly object _replacement;
private readonly Type _type;
public MaskValueProvider(PropertyInfo targetProperty, object replacement, Type type)
{
_targetProperty = targetProperty;
_replacement = replacement;
_type = type;
}
public object GetValue(object target)
{
return _replacement;
}
public void SetValue(object target, object value)
{
_targetProperty.SetValue(target, value);
}
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskStringValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(string);
public object Replacement { get; }
public MaskStringValueAttribute(string replacement)
{
Replacement = replacement;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskIntValueAttribute : Attribute, IMaskAttribute
{
public object Replacement { get; }
public Type Type => typeof(int);
public MaskIntValueAttribute(int replacement)
{
Replacement = replacement;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskGuidValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(Guid);
public object Replacement => Guid.Empty;
}
[AttributeUsage(AttributeTargets.Property)]
public class MaskDateTimeValueAttribute : Attribute, IMaskAttribute
{
public Type Type => typeof(DateTime);
public object Replacement => new DateTime(1970, 1, 1);
}
public interface IMaskAttribute
{
Type Type { get; }
object Replacement { get; }
}
}
I hope somebody will find it helpful.
You can try nuget package https://www.nuget.org/packages/Slin.Masking and https://www.nuget.org/packages/Slin.Masking.NLog.
It can easily be integrated with DotNet projects with slight changes, and you can define your rules for it. But the document needs some improvement.
As a suggestion, you can use two files:
masking.json (can be a generic one, that shared across all projects)
masking.custom.json (can be used with particular rules for specific projects)
I got the object below
public class ResponseMessage
{
public string Code { get; set; }
public string Msg { get; set; }
public Person[] Data { get; set; }
}
public class Person
{
public string name { get; set; }
public string age { get; set; }
public PersonDetail Detail { get; set; }
}
public class PersonDetail
{
public string Birthday { get; set; }
public string CellPhone { get; set; }
}
How to generate different level by user privilege,like level 1
{"Code":"0000","Msg":"OK"}
,level 2
{"Code":"0000","Msg":"OK","Data":[{"Alex","25"},{"Ben","30"}]}
I have tried to using settings(MaxDepth),but it seems not work.....
Is there any way that can hide data dynamically?
Use Conditional Property Serialization.
You have two options for implementing it:
If ResponseMessage can be modified (i.e., you can add a method to it) and if it has access to privilege level (e.g., via function call), supply a ShouldSerialize method.
For example:
public class ResponseMessage
{
public string Code { get; set; }
public string Msg { get; set; }
public Person[] Data { get; set; }
public bool ShouldSerializeData()
{
// Privileger.GetPrivilege() is a naive example of a method
// that gets you privilige level
return Privileger.GetPrivilege() > 1;
}
}
If not, go with a custom ContractResolver.
Here's a simple one:
public class PrivilegeContractResolver : DefaultContractResolver
{
public int privilege = -1;
public PrivilegeContractResolver(int privilege)
{
this.privilege = privilege;
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (property.PropertyName == "Data")
{
property.ShouldSerialize = dummy => privilege > 1;
}
return property;
}
}
Example usage:
public void Run()
{
ResponseMessage r = new ResponseMessage
{
Code = "some_code",
Msg = "some_message",
Data = new [] { new Person { name = "Joe" } }
};
var lowPriv = JsonConvert.SerializeObject(r, new JsonSerializerSettings
{
ContractResolver = new PrivilegeContractResolver(0)
});
var highPriv = JsonConvert.SerializeObject(r, new JsonSerializerSettings
{
ContractResolver = new PrivilegeContractResolver(2)
});
Console.WriteLine(lowPriv); // outputs: {"Code":"some_code","Msg":"some_message"}
Console.WriteLine(highPriv); // outputs: {"Code":"some_code","Msg":"some_message","Data":[{"name":"Joe","age":null,"Detail":null}]}
}
I have a list of objects (resultList) with a json root (testRoot) which needs to be serialized into json string. The list contains instances (TypeA, B, C) of different types which have their own serializerSettings.
How to implement a custom JsonConverter / JsonSerializerSettings so that when
I call JsonConvert.SerializeObject(testRoot), it should serialize each instance based on settings from that instance.
I am unable to find a good solution to my problem. Appreciate your help.
class TypeA
{
public string Name { get; set; }
public int intPropertyA { get; set; }
public string strPropertyA { get; set; }
public JsonSerializerSettings serializerSettingsA { get; set; }
}
class TypeB
{
public string Name { get; set; }
public int intPropertyB { get; set; }
public string strPropertyB { get; set; }
public JsonSerializerSettings serializerSettingsB { get; set; }
}
class TypeC
{
public string Name { get; set; }
public int intPropertyC { get; set; }
public string strPropertyC { get; set; }
public JsonSerializerSettings serializerSettingsC { get; set; }
}
static void Main(string[] args)
{
object[] resultList = new object[3];
int i = 0;
TypeA objA = new TypeA(); // assume all values initialized
TypeB objB = new TypeB(); // assume all values initialized
TypeC objC = new TypeC(); // assume all values initialized
resultList[i++] = new
{
Name = objA.Name,
IntValue = objA.intPropertyA,
StringValue = objA.strPropertyA
};
resultList[i++] = new
{
Name = objB.Name,
IntValue = objB.intPropertyB,
StringValue = objB.strPropertyB
};
resultList[i++] = new
{
Name = objC.Name,
IntValue = objC.intPropertyC,
StringValue = objC.strPropertyC
};
object testRoot = new
{
Test = "All the test results",
date = "",
Results = resultList
};
// How to customize the JsonSerializerSettings/JsonConverter so that, while serializing each type it should use settings from that instace.
string jsonStr = JsonConvert.SerializeObject(testRoot);
}
public class CompositeSerializerSettings : Newtonsoft.Json.JsonSerializerSettings
{
public CompositeSerializerSettings()
{
}
}
public class CompositeJsonConverter : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
As I noted in a comment above, this doesn't exactly answer your question, because in your test code you don't pass instances of TypeA or TypeB or TypeC, you pass anonymous objects derived from those classes.
However, if you are actually trying to serialize the objects themselves rather than anonymous objects, and want the serializer to use custom settings for each type, you have to tell it how to find them.
Using an interface is one option: note you have to 'ignore' the settings, or they appear in the serialized output - also I have set the indentation purely to show this is working.
public interface ISerializerSettings
{
JsonSerializerSettings serializerSettings { get;}
}
class TypeA : ISerializerSettings
{
public string Name { get; set; }
public int intPropertyA { get; set; }
public string strPropertyA { get; set; }
[JsonIgnore]
public JsonSerializerSettings serializerSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
}
class TypeB : ISerializerSettings
{
public string Name { get; set; }
public int intPropertyB { get; set; }
public string strPropertyB { get; set; }
[JsonIgnore]
public JsonSerializerSettings serializerSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.None
};
}
class TypeC : ISerializerSettings
{
public string Name { get; set; }
public int intPropertyC { get; set; }
public string strPropertyC { get; set; }
[JsonIgnore]
public JsonSerializerSettings serializerSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
}
Then in the converter, use the type's settings:
public class CompositeJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(ISerializerSettings).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteRaw(JsonConvert.SerializeObject(value, ((ISerializerSettings)value).serializerSettings));
}
}
Finally, in your program, tell the serializer to use your converter:
string jsonStr = JsonConvert.SerializeObject(testRoot, new CompositeJsonConverter());
Given this input:
TypeA objA = new TypeA(); // assume all values initialized
TypeB objB = new TypeB(); // assume all values initialized
TypeC objC = new TypeC(); // assume all values initialized
object[] resultList = new object[] {
objA, objB, objC
};
object testRoot = new
{
Test = "All the test results",
date = "",
Results = resultList
};
string jsonStr = JsonConvert.SerializeObject(testRoot, new CompositeJsonConverter());
This is the output: note that TypeB has no formatting, but the others do:
{"Test":"All the test results","date":"","Results":[{
"Name": null,
"intPropertyA": 0,
"strPropertyA": null
}{"Name":null,"intPropertyB":0,"strPropertyB":null}{
"Name": null,
"intPropertyC": 0,
"strPropertyC": null
}]}
I have a Web Api Controller like this one :
public IHttpActionResult Create(PaymentDTO Payment)
My DTOs are:
public class PaymentDTO
{
public int Id { get; set; }
public string type { get; set; }
public IEnumerable<TransactionDTO> Transactions { get; set; }
}
public class TransactionDTO
{
public int Id { get; set; }
public string Description { get; set; }
public string CreateTime { get; set; }
public string UpdateTime { get; set; }
}
public class SaleDTO : TransactionDTO
{
public string Total { get; set; }
public string Currency{ get; set; }
}
public class OrderDTO : TransactionDTO
{
public string State {get;set;}
}
I receive the following JSON formatted data :
{
"Type": "sale",
"Id": 101,
"transactions": [
{
"Total": "30.50",
"Currency": "USD",
"Description": "transaction description"
}
]
}
I want JSON.net to instantiate either a IEnumerable<SaleDTO> or IEnumerable<OrderDTO> based on the Type Property.
I could've used a custom type converter, but only if Type property was in TransactionDTO. But I want the Type property to be in the parent object (PaymentDTO)
Thank you in advance for your help.
You can do this with a custom JsonConverter on the PaymentDTO class:
public class PaymentDTOConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(PaymentDTO).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader);
var payment = (PaymentDTO)existingValue ?? new PaymentDTO();
// Extract the transactions.
var transactions = obj.Property("transactions") ?? obj.Property("Transactions");
if (transactions != null)
transactions.Remove();
// Populate the remaining regular properties.
using (var subReader = obj.CreateReader())
serializer.Populate(subReader, payment);
if (transactions != null)
{
// Deserialize the transactions list.
var type = PaymentDTO.GetTransactionDTOType(payment.type) ?? typeof(TransactionDTO);
using (var subReader = transactions.Value.CreateReader())
// Here we are taking advantage of array covariance.
payment.Transactions = (IEnumerable<TransactionDTO>)serializer.Deserialize(subReader, type.MakeArrayType());
}
return payment;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then apply it to your PaymentDTO class as follows:
[JsonConverter(typeof(PaymentDTOConverter))]
public class PaymentDTO
{
static Dictionary<string, Type> namesToTransactions;
static Dictionary<Type, string> transactionsToNames = new Dictionary<Type, string>
{
{ typeof(SaleDTO), "sale" },
{ typeof(OrderDTO), "order" },
};
static PaymentDTO()
{
namesToTransactions = transactionsToNames.ToDictionary(p => p.Value, p => p.Key);
}
public static string GetTransactionDTOTypeName<TTransactionDTO>() where TTransactionDTO : TransactionDTO
{
string name;
if (transactionsToNames.TryGetValue(typeof(TTransactionDTO), out name))
return name;
return null;
}
public static Type GetTransactionDTOType(string name)
{
Type type;
if (namesToTransactions.TryGetValue(name, out type))
return type;
return null;
}
public int Id { get; set; }
public string type { get; set; }
[JsonProperty("transactions")]
public IEnumerable<TransactionDTO> Transactions { get; set; }
}
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.
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!");
}
}
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);
}
}
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; }
}
}