Custom JSON serialization of sensitive data for PCI compliance - c#

We have to log incoming requests and outgoing responses for our web service. This includes JSON serialization of each object so they can be stored in a database.
Some information is considered sensitive (such as Social Security Numbers, credit card numbers, etc.) and we cannot include these in our logs per PCI compliance. Right now we're manually replacing the values with a placeholder value (e.g. "[PRIVATE]") but this only works with string properties. Some data, such as a Date of Birth is not stored as a string so this doesn't work as the replacement of the property value happens before the serialization. The big problem is that it is too easy for someone to forget to do remove the sensitive data before logging it, which is highly undesirable.
To remedy this, I was thinking of creating a custom attribute and placing it on the property and then having the JSON serialization routine look for this attribute on each property and if it exists, replace the serialized value with a placeholder such as "[PRIVATE]".
Right now we are using the System.Web.Script.Serialization.JavaScriptSerializer for our serialization. Obviously it knows nothing of my custom attribute. How would I go about changing the serialization process so any data decorated with my custom "SensitiveData" attribute is replaced with a placeholder value? I'm not against using a different serializer but was hoping I could leverage the features of an existing one instead of writing my own.

Here's my solution, although it may need minor tweaks:
My custom JsonConverter:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Reflection;
public class SensitiveDataJsonConverter : JsonConverter
{
private readonly Type[] _types;
public SensitiveDataJsonConverter(params Type[] types)
{
_types = types;
}
public override bool CanConvert(Type objectType)
{
return _types.Any(e => e == 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)
{
var jObject = new JObject();
var type = value.GetType();
foreach (var propertyInfo in type.GetProperties())
{
// We can only serialize properties with getters
if (propertyInfo.CanRead)
{
var sensitiveDataAttribute = propertyInfo.GetCustomAttribute<SensitiveDataAttribute>();
object propertyValue;
if (sensitiveDataAttribute != null)
propertyValue = "[REDACTED]";
else
propertyValue = propertyInfo.GetValue(value);
if (propertyValue == null)
propertyValue = string.Empty;
var jToken = JToken.FromObject(propertyValue, serializer);
jObject.Add(propertyInfo.Name, jToken);
}
}
jObject.WriteTo(writer);
}
Here's my custom attribute:
using System;
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class SensitiveDataAttribute : Attribute
{
}
We use it like this:
string serialized;
try
{
serialized = JsonConvert.SerializeObject(value, new SensitiveDataJsonConverter(value.GetType()));
}
catch // Some objects cannot be serialized
{
serialized = $"[Unable to serialize object '{key}']";
}
Here's a test class I try to serialize:
class Person
{
public Person()
{
Children = new List<Person>();
}
public List<Person> Children { get; set; }
[SensitiveData]
public DateTime DateOfBirth { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[SensitiveData]
public string SocialSecurityNumber { get; set; }
public Person Spouse { get; set; }
}
This seemed to work great until I added the Spouse and Children properties but I was getting a NullReferenceException. I added this to the WriteJson method which corrected the problem:
if (propertyValue == null)
propertyValue = string.Empty;

I was also looking for a way to hide GDPR data in Audit information. So i came across this question.
I've had a lot of issues implementing this JsonConvertor. I had a lot of issues with types and certainly with child types (i was serializing the AuditEvent from Audit.NET lib, and then some child props of that). I even added the code from #Duu82, but then still a lot of issues.
I however found another way of resolving my problem, and it was in another question/answer on SO: Replace sensitive data value on JSON serialization
So for future users who come across this, I found that answer more practical, less code and working

#DesertFoxAZ had a great answer in here. To augment that answer, I would add in the below code. Without this code, the PII redaction will only apply to the top-level class that you pass into your constructor. If you have properties that are objects that should also have some of their data redacted, you need the below code as well.
The code below was modified from an answer in this SO post.
private readonly List<Type> _alreadyVisitedTypes = new List<Type>(); //avoid infinite recursion
private void RecursivelySetTypesToOperateOn(Type currentType)
{
if (_alreadyVisitedTypes.Contains(currentType))
{
return;
}
_alreadyVisitedTypes.Add(currentType);
if (currentType.IsClass && currentType.Namespace != "System") //custom defined classes only
{
_types.Add(currentType);
}
foreach (PropertyInfo pi in currentType.GetProperties())
{
if (pi.PropertyType.IsClass)
{
RecursivelySetTypesToOperateOn(pi.PropertyType);
}
}
}

I also wrote a recursive version of #DesertFoxAZ's answer for multi-level objects.
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var writingObject = RedactSensitiveProperties(value, serializer);
writingObject.WriteTo(writer);
}
private static JObject RedactSensitiveProperties(object value, JsonSerializer serializer)
{
var writingObject = new JObject();
var type = value.GetType();
foreach (var propertyInfo in type.GetProperties())
{
// We can only serialize properties with getters
if (propertyInfo.CanRead)
{
var sensitiveDataAttribute = propertyInfo.GetCustomAttribute<SensitiveDataAttribute>();
object propertyValue;
if (sensitiveDataAttribute != null)
{
propertyValue = "[REDACTED]";
}
else
{
propertyValue = propertyInfo.GetValue(value);
}
if (propertyValue == null)
{
propertyValue = string.Empty;
}
var jToken = JToken.FromObject(propertyValue, serializer);
if (jToken.Type == JTokenType.Object)
{
var jObject = RedactSensitiveProperties(propertyValue, serializer);
writingObject.Add(propertyInfo.Name, jObject);
}
else if (jToken.Type == JTokenType.Array)
{
var jArray = new JArray();
foreach (var o in (IEnumerable<object>)propertyValue)
{
jArray.Add(RedactSensitiveProperties(o, serializer));
}
writingObject.Add(propertyInfo.Name, jArray);
}
else
{
writingObject.Add(propertyInfo.Name, jToken);
}
}
}
return writingObject;
}

Related

Deserializing json - mapping single property with no direct json match

I am trying to deserialize an existing JSON structure to into an object composed of a set of models. The naming in these models are not consistent and I was specifically asked to not change them (renaming, adding attributes, etc).
So, given this Json text (just a small sample):
{
"parameter": {
"alarms": [
{
"id": 1,
"name": "alarm1",
"type": 5,
"min": 0,
"max": 2
}],
"setting-active": true,
"setting-oneRun": true
}
}
would need to be mapped into these models:
public class Alarm
{
public int AlarmId { get; set; }
public string AlarmName { get; set; }
public AlarmType RbcType { get; set; }
public int MinimumTolerated { get; set; }
public int MaximumTolerated { get; set; }
}
public class Setting
{
public bool Active { get; set; }
public bool OneRun { get; set; }
}
public class Parameter
{
public List<Alarm> Alarms { get; set; }
public Setting ParameterSetting { get; set; }
}
So far, im writing a class that extends DefaultContractResolver and overrides maps property names.
MyCustomResolver so far:
public class MyCustomResolver : DefaultContractResolver
{
private Dictionary<string, string>? _propertyMappings;
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
//ModelMappings is a static class that will return a dictionary with mappings per ObjType being deserialized
_propertyMappings = ModelMappings.GetMapping(type);
return base.CreateProperties(type, memberSerialization);
}
protected override string ResolvePropertyName(string propertyName)
{
if (_propertyMappings != null)
{
_propertyMappings.TryGetValue(propertyName, out string? resolvedName);
return resolvedName ?? base.ResolvePropertyName(propertyName);
}
return base.ResolvePropertyName(propertyName);
}
}
Code that Im using to deserialize:
var settings = new JsonSerializerSettings();
settings.DateFormatString = "YYYY-MM-DD";
settings.ContractResolver = new MyCustomResolver();
Parameter p = JsonConvert.DeserializeObject<Parameter>(jsonString, settings);
So I reached a point I need to somehow map the properties in Parameter to values located in the prev json node ("setting-active", "setting-oneRun"). I need to tell the deserializer where these values are.
Can this be done using an extension of DefaultContractResolver ?
I appreciate any tips pointing in the right direction
You can apply ModelMappings.GetMapping(objectType) in DefaultContractResolver.CreateObjectContract():
public class MyCustomResolver : DefaultContractResolver
{
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
var overrides = ModelMappings.GetMapping(objectType);
if (overrides != null)
{
foreach (var property in contract.Properties.Concat(contract.CreatorParameters))
{
if (property.UnderlyingName != null && overrides.TryGetValue(property.UnderlyingName, out var name))
property.PropertyName = name;
}
}
return contract;
}
}
Notes:
By applying the mappings in CreateObjectContract() you can remap both property names and creator parameter names.
Since the contract resolver is designed to resolve contracts for all types, storing a single private Dictionary<string, string>? _propertyMappings; doesn't really make sense.
Unlike your previous question, your current question shows properties from a nested c# object ParameterSetting getting percolated up to the parent object Parameter. Since a custom contract resolver is designed to generate the contract for a single type, it isn't suited to restructuring data between types. Instead, consider using a DTO or converter + DTO in such situations:
public class ParameterConverter : JsonConverter<Parameter>
{
record ParameterDTO(List<Alarm> alarms, [property: JsonProperty("setting-active")] bool? Active, [property: JsonProperty("setting-oneRun")] bool? OneRun);
public override void WriteJson(JsonWriter writer, Parameter? value, JsonSerializer serializer)
{
var dto = new ParameterDTO(value!.Alarms, value.ParameterSetting?.Active, value.ParameterSetting?.OneRun);
serializer.Serialize(writer, dto);
}
public override Parameter? ReadJson(JsonReader reader, Type objectType, Parameter? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var dto = serializer.Deserialize<ParameterDTO>(reader);
if (dto == null)
return null;
existingValue ??= new ();
existingValue.Alarms = dto.alarms;
if (dto.Active != null || dto.OneRun != null)
existingValue.ParameterSetting = new () { Active = dto.Active.GetValueOrDefault(), OneRun = dto.OneRun.GetValueOrDefault() };
return existingValue;
}
}
If your "real" model is too complex to define a DTO, you could create a JsonConverter<Paramater> that (de)serializes the JSON into an intermediate JToken hierarchy, then restructures that. See e.g. this answer to Can I serialize nested properties to my class in one operation with Json.net?.
In some cases, the custom naming of your properties is just camel casing. To camel case property names without the need for explicit overrides, set MyCustomResolver.NamingStrategy to CamelCaseNamingStrategy e.g. as follows:
var settings = new JsonSerializerSettings
{
DateFormatString = "YYYY-MM-DD",
// Use CamelCaseNamingStrategy since many properties in the JSON are just camel-cased.
ContractResolver = new MyCustomResolver { NamingStrategy = new CamelCaseNamingStrategy() },
Converters = { new ParameterConverter() },
};
Demo fiddle here.
I think that the best way to "KEEP IT SIMPLE", you need to define an object that has exactly the properties of the json. Then you can use a library like "Automapper" to define rules of mapping between the "json object" and the "business object".

Converting a POCO object with JsonPropertyName decorations into a URL query string

Is there an .NET API to convert a POCO object with JsonPropertyName decorations into a properly encoded URL query string?
For example, for:
public record AuthEndPointArgs
{
[JsonPropertyName("response_type")]
public string? ResponseType { get; set; } // "code"
[JsonPropertyName("client_id")]
public string? ClientId { get; set; } // "a:b"
// ...
}
I expect: ?response_type=code&client_id=a%3Ab.
A home-grown version I'm using for now:
/// <summary>
/// Convert a shallow POCO object into query string,
/// with JsonPropertyName decorations becoming query string argument names
/// </summary>
public static string ConvertObjectToQueryString(object source, string query)
{
var uriArgs = System.Web.HttpUtility.ParseQueryString(query);
var jsonNode = System.Text.Json.JsonSerializer.SerializeToNode(source) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
foreach (var item in jsonNode.AsObject())
{
uriArgs[item.Key] = item.Value?.ToString() ?? String.Empty;
}
return uriArgs.ToString() ?? String.Empty;
}
Updated, for completeness, a reverse conversion:
/// <summary>
/// Convert a query string into a POCO object with string properties,
/// decorated with JsonPropertyName attributes
/// </summary>
public static T ConvertQueryStringToObject<T>(string query)
{
var args = System.Web.HttpUtility.ParseQueryString(query);
var jsonObject = new JsonObject();
foreach (var key in args.Keys.Cast<string>())
{
jsonObject.Add(key, JsonValue.Create(args[key]));
}
return jsonObject.Deserialize<T>() ??
throw new InvalidOperationException(typeof(T).Name);
}
To my knowledge there is no native .NET API, it seems like ASP.NET (Core) has support for reading it to some extent though (check here and here) but I can't tell how to create one.
The laziest solution would probably be to just serialize your object to JSON, and then HttpUtility.UrlEncode(json), then pass that to a query param, which would like so:
&payload=%7B%22response_type%22%3A%20%22code%22%2C%22client_id%22%3A%20%22a%3Ab%22%7D
At the other end just JsonSerializer.Deserialize<AuthEndPointArgs>(HttpUtility.UrlDecode(payload)) like so. This is assuming you can edit both ends.
While it sounds kinda stupid, it works, at in certain terms may even be better than serializing your AuthEndPointArgs to a query string directly, because the standard for a query string lacks some definitions, like how to deal with arrays, also complex options. It seems like the JS and PHP community have unofficial standards, but they require a manual implementation on both ends. So we'll also need to roll our own "standard" and implementation, unless we say that we can only serialize an object that fulfils the following criteria:
No complex objects as properties
No lists/ arrays as properties
Side note: URLs have a maximum length depending on a lot of factors, and by sending complex objects via query parameters you may go above that limit pretty fast, see here for more on this topic. It may just be best to hardcode something like ToQueryParams like Ady suggested in their answer
If we do want a generic implementation that aligns with those criteria, our implementation is actually quite simple:
public static class QueryStringSerializer
{
public static string Serialize(object source)
{
var props = source.GetType().GetProperties(
BindingFlags.Instance | BindingFlags.Public
);
var output = new StringBuilder();
foreach (var prop in props)
{
// You might want to extend this check, things like 'Guid'
// serialize nicely to a query string but aren't primitive types
if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string))
{
var value = prop.GetValue(source);
if (value is null)
continue;
output.Append($"{GetNameFromMember(prop)}={HttpUtility.UrlEncode(value.ToString())}");
}
else
throw new NotSupportedException();
}
}
}
private static string GetNameFromMember(MemberInfo prop)
{
string propName;
// You could also implement a 'QueryStringPropertyNameAttribute'
// if you want to be able to override the name given, for this you can basically copy the JSON attribute
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyNameAttribute.cs
if (Attribute.IsDefined(prop, typeof(JsonPropertyNameAttribute)))
{
var attribute = Attribute.GetCustomAttribute(prop, typeof(JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
// This check is technically unnecessary, but VS wouldn't shut up
if (attribute is null)
propName = prop.Name;
else
propName = attribute.Name;
}
else
propName = prop.Name;
return propName;
}
If we want to support objects with enumerables as properties or with "complex" objects as members we need to define how to serialize them, something like
class Foo
{
public int[] Numbers { get; set; }
}
Could be serialized to
?numbers[]=1&numbers[]=2
Or to a 1 indexed "list"
?numbers[1]=1&numbers[2]=2
Or to a comma delimited list
?numbers=1,2
Or just multiple of one instance = enumerable
?numbers=1&numbers=2
And probably a lot more formats. But all of these are framework/ implementation specific of whatever is receiving these calls as there is no official standard, and the same goes for something like
class Foo
{
public AuthEndPointArgs Args { get; set; }
}
Could be
?args.response_type=code&args.client_id=a%3Ab
And a bunch more different ways I can't be bothered to think off right now
one possibility is System.Text.Json.Serialization.JsonConverter<T> documented in How to customize property names and values with System.Text.Json
class AuthEndpointMarshaller : JsonConverter<AuthEndPointArgs>
{
public override AuthEndPointArgs? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
//TODO check typeToConvert
AuthEndPointArgs result = new();
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return result;
}
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string ? propertyName = reader.GetString();
reader.Read();
string? propertyValue = reader.GetString();
switch (propertyName)
{
case "response_type":
result.ResponseType = propertyValue;
break;
case "client_id":
result.ClientId = HttpUtility.UrlDecode(propertyValue);
break;
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, AuthEndPointArgs value, JsonSerializerOptions options)
{
value.ClientId = HttpUtility.UrlEncode(value.ClientId);
JsonSerializer.Serialize(writer,value);
}
}
You then call it using
string ConvertShallowObjectToQueryStringAlternate(AuthEndPointArgs source)
{
var options = new JsonSerializerOptions()
{
Converters =
{
new AuthEndpointMarshaller()
}
};
var jsonNode = JsonSerializer.SerializeToNode(source, options) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
return jsonNode.ToString();
}
or if you are looking for a quick solution
public record AuthEndPointArgs
{
// ...
public string ToQueryParams()
{
var sb = new StringBuilder($"?response_type={ResponseType}&client_id={HttpUtility.UrlEncode(ClientId)}");
return sb.ToString();
}
}

Newtonsoft.Json convertion xml document to json with array detection

what I am trying to do is to convert complex xml document to json format, and I am using Newtonsoft.Json to achieve my goal. and I have came across small - big problem. So for example
I have a model that looks like:
public class assets
{
public UInt32 id {get; set;}
public String providerName {get; set;}
public String provider {get; set;}
public String realm {get; set;}
public ICollection<unit> unit {get; set;}
}
My intention is that user will stream xml content to method that will change that xml to json and i will post it to API.
To simplify User is pasting simple xml (normal xml is far more complex, but basically it would looks like many levels of example bellow)
<assets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="assets.xsd" providerName="myProviderName" provider="myProvider" realm="myCatalog">
<unit idKey="newGeo_63119679"></unit>
<unit idKey="newGeo_63119179"></unit>
</assets>
Json result will look like:
{"#providerName":"myProviderName","#provider":"myProvider","#realm":"myCatalog","unit":[{"#idKey":"newGeo_63119679"},{"#idKey":"newGeo_63119577"}]}
So service that does all the magic looks like:
public async ValueTask<string> AddAsset(string body)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(body);
string json = JsonConvert.SerializeXmlNode(doc, Formatting.None, true);
HttpResponseMessage response = await this._httpClient.PostAsJsonAsync("/Asset/create_asset", json);
string responseJson = await response.Content.ReadAsStringAsync();
return responseJson;
}
Well OK this part works, but when I remove from xml one unit node (so only one unit node is left), my result is:
{"#providerName":"myProviderName","#provider":"myProvider","#realm":"myCatalog","unit":{"#idKey":"newGeo_63119679"}}
And now so needed array to deserialize it to model is gone. I know I could manipulate xml attributes to add json:Array='true'.
But I was wondering if there is more complex solution for example JsonConverter that can take search for property in given type and check it if its collection and if so assign it as json collection. How can I bite this problem?
And Also as I checked SerializeXmlNode has no converter parameter.
Well, I found my answer, by mixing information from #dbc and my happy innovation:
public class Converter<TEntity> : JsonConverter<TEntity> where TEntity : class
{
private readonly IEnumerable<Type> _entityTypes =
Assembly.GetExecutingAssembly().GetReferencedAssemblies()
.SelectMany(assembly => Assembly.Load(assembly).GetTypes().Where(t => t.Namespace == MigrationService.EntitiesNameSpace));
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, TEntity value, JsonSerializer serialize) =>
throw new NotImplementedException($"Write option is turned off for {nameof(CollectionConverter)} custom json converter");
public override TEntity ReadJson(JsonReader reader, Type objectType, TEntity existingValue, bool hasExistingValue, JsonSerializer serializer)
{
void SetSimpleTypes(JToken token, TEntity instance, PropertyInfo property)
{
var value = token[property.Name];
if(value == null) return;
var cast = Convert.ChangeType(value, property.PropertyType);
property.SetValue(instance, cast);
}
void SetClassTypes(JToken token, TEntity instance, PropertyInfo property)
{
var constructedConverterType = typeof(CollectionConverter<>).MakeGenericType(property.PropertyType);
if (Activator.CreateInstance(constructedConverterType) is not JsonConverter converter) return;
var propertyInstance = JsonConvert.DeserializeObject(token[property.Name].ToString(), property.PropertyType, converter);
property.SetValue(instance, propertyInstance);
}
void SetCollectionTypes(JToken token, TEntity instance, PropertyInfo property)
{
var constructedCollectionType = typeof(List<>).MakeGenericType(property.PropertyType.GetGenericArguments().Single());
var collectionInstance = Activator.CreateInstance(constructedCollectionType) as IList;
var value = token[property.Name];
void HandleSingleCollectionType(string body)
{
var propertyCollectionType = property.PropertyType.GetGenericArguments().Single();
var constructedConverterType = typeof(CollectionConverter<>).MakeGenericType(propertyCollectionType);
if (Activator.CreateInstance(constructedConverterType) is not JsonConverter converter) return;
var convertedInstance = JsonConvert.DeserializeObject(body, propertyCollectionType, converter);
collectionInstance?.Add(convertedInstance);
}
switch (value)
{
case JArray array:
foreach (var body in array)
{
HandleSingleCollectionType(body.ToString());
}
break;
case JObject:
HandleSingleCollectionType(value.ToString());
break;
default:
Debug.WriteLine($"Unknown or empty json token value for property {property.Name}");
break;
}
property.SetValue(instance, collectionInstance);
}
JToken token = JToken.Load(reader);
var instance = (TEntity)Activator.CreateInstance(typeof(TEntity));
var properties = instance?.GetType().GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
if (properties == null) return default;
foreach (PropertyInfo property in properties)
{
try
{
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
SetCollectionTypes(token, instance, property);
}
else if (this._entityTypes.Any(type => type == property.PropertyType) && property.PropertyType.IsClass)
{
SetClassTypes(token, instance, property);
}
else
{
SetSimpleTypes(token, instance, property);
}
}
catch (NullReferenceException ex)
{
Debug.WriteLine($"Null value for entity class property {property.Name}, exception: {ex}");
}
catch (FormatException ex)
{
Debug.WriteLine($"Can not convert value property {property.Name} in {instance.GetType().Name}, exception: {ex}");
}
catch (Exception ex)
{
Debug.WriteLine($"Undefined exception for {property.Name} exception: {ex}");
}
}
return instance;
}
}
It converts very complex xml structures based on class structure so if class structure says something is collection it will build collection, if its an object (I mean class) it will create instance of a class and so on. Additionally it uses itself to convert any child xml has.

OnDeserialized callback with JsonConverter

I´m trying to use the JsonConverter from this answer to Can I specify a path in an attribute to map a property in my class to a child property in my JSON? by Brian Rogers to map nested properties in JSON to a flat object.
The converter works well, but I need to fire the OnDeserialized callback to fill other properties and it´s not fired. If I don´t use the converter, the callback is fired.
Examples:
string json = #"{
'response': {
'code': '000',
'description': 'Response success',
},
'employee': {
'name': 'Test',
'surname': 'Testing',
'work': 'At office'
}
}";
// employee.cs
public class EmployeeStackoverflow
{
[JsonProperty("response.code")]
public string CodeResponse { get; set; }
[JsonProperty("employee.name")]
public string Name { get; set; }
[JsonProperty("employee.surname")]
public string Surname { get; set; }
[JsonProperty("employee.work")]
public string Workplace { get; set; }
[OnDeserialized]
internal void OnDeserializedMethod(StreamingContext context)
{
Workplace = "At Home!!";
}
}
// employeeConverter.cs
public class EmployeeConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
If I add [JsonConverter(typeof(EmployeeConverter))] in the Employee class I obtain:
=== With Converter ===
Code: 000
Name: Test
Surname: Testing
Workplace: At office
If I remove[JsonConverter(typeof(EmployeeConverter))] from the Employee class I obtain:
=== With Converter ===
Code:
Name:
Surname:
Workplace: At Home!!
My goal is to obtain:
=== With Converter ===
Code: 000
Name: Test
Surname: Testing
Workplace: At Home!!
Is the converter missing something?
Once you have created a custom JsonConverter for a type, it is incumbent on the converter to handle everything that needs to be done during deserialization -- including
Calling serialization callbacks.
Skipping ignored properties.
Invoking JsonConverter.ReadJson() for converters attached via attributes to members of the type.
Setting default values, skipping null values, resolving references, etc etc.
The complete logic can be seen in JsonSerializerInternalReader.PopulateObject(), and in theory you might need to make your ReadJson() method duplicate this method. (But in practice you will likely only implement a small, necessary subset of the logic.)
One way to make this task easier is to use Json.NET's own JsonObjectContract type metadata, as returned by JsonSerializer.ContractResolver.ResolveContract(objectType). This information contains the list of serialization callbacks and JsonpropertyAttribute property data used by Json.NET during deserialization. A modified version of the converter that uses this information would be as follows:
// Modified from this answer https://stackoverflow.com/a/33094930
// To https://stackoverflow.com/questions/33088462/can-i-specify-a-path-in-an-attribute-to-map-a-property-in-my-class-to-a-child-pr/
// By https://stackoverflow.com/users/10263/brian-rogers
// By adding handling of deserialization callbacks and some JsonProperty attributes.
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract ?? throw new JsonException(string.Format("{0} is not a JSON object", objectType));
var jo = JToken.Load(reader);
if (jo.Type == JTokenType.Null)
return null;
else if (jo.Type != JTokenType.Object)
throw new JsonSerializationException(string.Format("Unexpected token {0}", jo.Type));
var targetObj = contract.DefaultCreator();
// Handle deserialization callbacks
foreach (var callback in contract.OnDeserializingCallbacks)
callback(targetObj, serializer.Context);
foreach (var property in contract.Properties)
{
// Check that property isn't ignored, and can be deserialized.
if (property.Ignored || !property.Writable)
continue;
if (property.ShouldDeserialize != null && !property.ShouldDeserialize(targetObj))
continue;
var jsonPath = property.PropertyName;
var token = jo.SelectToken(jsonPath);
// TODO: default values, skipping nulls, PreserveReferencesHandling, ReferenceLoopHandling, ...
if (token != null && token.Type != JTokenType.Null)
{
object value;
// Call the property's converter if present, otherwise deserialize directly.
if (property.Converter != null && property.Converter.CanRead)
{
using (var subReader = token.CreateReader())
{
if (subReader.TokenType == JsonToken.None)
subReader.Read();
value = property.Converter.ReadJson(subReader, property.PropertyType, property.ValueProvider.GetValue(targetObj), serializer);
}
}
// TODO: property.ItemConverter != null
else
{
value = token.ToObject(property.PropertyType, serializer);
}
property.ValueProvider.SetValue(targetObj, value);
}
}
// Handle deserialization callbacks
foreach (var callback in contract.OnDeserializedCallbacks)
callback(targetObj, serializer.Context);
return targetObj;
}
Demo fiddle here.

Deserialize two values into the same property

I have a client which can call two different versions of a service.
One service only sends a single value:
{
"value" : { ... }
}
The second service always returns multiple values:
{
"values" : [
{ ... },
{ ... }
]
}
Ideally, I'd like to represent this with a single object in my client classes so the user never sees whether it's a single value or multiple values.
public class MyValues
{
public List<Stuff> Values { get; set; }
public Thing Other { get; set; }
}
I think that the only way I'll be able to accomplish this is with a custom JsonConverter class which I apply to MyValues, but I really only want to do something custom when I'm deserializing the property value. I can't seem to figure out if an IContractResolver would be a better way to go (e.g. somehow attach a phantom property to MyValues that deserializes value and puts it into Values.
If I create a custom converter, how to I tell it to deserialize everything else normally (e.g. if Other has an extra properties make sure they are handled appropriately, etc.)
Rather than writing a JsonConverter, you could make a set-only property Value on your MyValues, like so:
public class MyValues
{
[JsonProperty]
Stuff Value
{
set
{
(Values = Values ?? new List<Stuff>(1)).Clear();
Values.Add(value);
}
}
public List<Stuff> Values { get; set; }
public Thing Other { get; set; }
}
It could be public or private if marked with [JsonProperty]. In this case Json.NET will call the Value setter if the singleton "value" property is encountered in the JSON, and call the Values setter if the array "values" property is encountered. Since the property is set-only only the array property will be re-serialized.
To make a custom JsonConverter that has special processing for a few properties of a type but uses default processing for the remainder, you can load the JSON into a JObject, detach and process the custom properties, then populate the remainder from the JObject with JsonSerializer.Populate(), like so:
class MyValuesConverter : CustomPropertyConverterBase<MyValues>
{
protected override void ProcessCustomProperties(JObject obj, MyValues value, JsonSerializer serializer)
{
// Remove the value property for manual deserialization, and deserialize
var jValue = obj.GetValue("value", StringComparison.OrdinalIgnoreCase).RemoveFromLowestPossibleParent();
if (jValue != null)
{
(value.Values = value.Values ?? new List<Stuff>()).Clear();
value.Values.Add(jValue.ToObject<Stuff>(serializer));
}
}
}
public abstract class CustomPropertyConverterBase<T> : JsonConverter where T : class
{
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var jObj = JObject.Load(reader);
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var value = existingValue as T ?? (T)contract.DefaultCreator();
ProcessCustomProperties(jObj, value, serializer);
// Populate the remaining properties.
using (var subReader = jObj.CreateReader())
{
serializer.Populate(subReader, value);
}
return value;
}
protected abstract void ProcessCustomProperties(JObject obj, T value, JsonSerializer serializer);
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public static class JsonExtensions
{
public static JToken RemoveFromLowestPossibleParent(this JToken node)
{
if (node == null)
return null;
var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
if (contained != null)
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (node.Parent is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}
}

Categories

Resources