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.
Related
I have been working to convert MongoDB BSON documents to List object in c#.
While converting, i get below error
"{"Unexpected character encountered while parsing value: O. Path '_id', line 1, position 10."}"
After searching for similar issues in stackoverflow, i found below link
JSON.NET cast error when serializing Mongo ObjectId
and i followed the same.
My Code:
Sample Entity/Model
public class BTMObj
{
[JsonConverter(typeof(MongoDataSerializer))]
public ObjectId _id { get; set; }
public string requestFormat { get; set; }
}
public class MongoDataSerializer : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ObjectId);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.String)
{
throw new Exception(
String.Format("Unexpected token parsing ObjectId. Expected String, got {0}.",
reader.TokenType));
}
var value = (string)reader.Value;
return String.IsNullOrEmpty(value) ? ObjectId.Empty : new ObjectId(value);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is ObjectId)
{
var objectId = (ObjectId)value;
writer.WriteValue(objectId != ObjectId.Empty ? objectId.ToString() : String.Empty);
}
else
{
throw new Exception("Expected ObjectId value.");
}
}
}
public List<T> GetMongoCollection<T>(string collectionName)
{
try
{
List<T> list = new List<T>();
var client = new MongoClient(Convert.ToString(ConfigurationManager.AppSettings["MONGO_CONNECTION"]));
var database = client.GetDatabase(Convert.ToString(ConfigurationManager.AppSettings["MONGO_DB"]));
var collection = database.GetCollection<BsonDocument>(collectionName);
var documents = collection.Find(new BsonDocument()).ToList();
foreach (var document in documents)
{
try
{
list.Add(JsonConvert.DeserializeObject<T>(document.ToJson()));
}
catch (Exception ex)
{
}
}
return list;
}
catch (Exception ex)
{
throw;
}
}
Call to Method
list = mongoDBOperations.GetMongoCollection<BTMObj>(Collection);
MongoDataSerializer class overridden methods should get call which is not the case.
Our need is to get ObjectId as string in Model.
Please help in resolving this issue.
Sample document.toJson() value
{
"_id": ObjectId("611cf42e1e4c89336b6fe2f0"),
"requestFormat": "json"
}
You only used half of the relevant code.
If you write this JSON:
"_id": ObjectId("611cf42e1e4c89336b6fe2f0"),
"requestFormat": "json"
}
As BsonObject.ToJson() does, then that's not JSON. The ObjectId(...) dialect, just as Date(), NumberLong(), NumberInt() and NumberDecimal() are constructs that make MongoDB spit out invalid JSON, because it's a representation of its internal BSON storage format.
So if you want to treat it as JSON, write valid JSON. The code is right there in the link: you need to serialize the object yourself. See How to deserialize a BsonDocument object back to class.
First make sure the Mongo C# driver deserializes the BSON into your POCO:
// Prefer using statically-typed extension methods such as
// _collection.FindAs<MyType>()
var deserialized = BsonSerializer.Deserialize<BTMobj>(document);
Then serialize that object to JSON using your converter:
var json = JsonConvert.SerializeObject(deserialized);
Your output will then become the very parseable:
"_id": "611cf42e1e4c89336b6fe2f0",
"requestFormat": "json"
}
And your type's metadata (attributes) will tell the parser to parse "611cf42e1e4c89336b6fe2f0" as a BsonObjectId when you try to deserialize it into a BTMobj again.
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.
A few of my API endpoints have models that include enums. FluentValidation is being used to verify that the values sent across meet their respective requirements.
To aid in usability and document generation, enums are allowed to be sent as strings rather than integers. Validation that the value sent across is in the correct range works fine if an invalid integer is sent, but serialization will fail if an invalid string is sent across.
public enum Foo
{
A = 1,
B = 2
}
public class Bar
{
public Foo? Foo {get;set;}
}
void Main()
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
var jsonString = "{\"foo\": \"C\"}";
var jsonSpan = (ReadOnlySpan<byte>)Encoding.UTF8.GetBytes(jsonString);
try
{
var result = JsonSerializer.Deserialize<Bar>(jsonSpan, options);
Console.WriteLine(result.Foo == null);
}
catch(Exception ex)
{
Console.WriteLine("Serialization Failed");
}
}
My desired outcome would be to simply deserialize the enum property to null when the string does not match any of the enum's fields so that the model can be passed through to the validator to create a friendly message.
How can I achieve this? This is using net-core 3 preview 8 with the System.Text.Json API.
As far I have tried, I have 2 solutions, one using System.Text.Json and the other one is Newtonsoft.
System.Text.Json
Your create a custom class using JsonConverter
You introduce Unknown enum in Foo.
in stead of using JsonStringEnumConverter
options.Converters.Add(new JsonStringEnumConverter());
Use your customized class CustomEnumConverter
options.Converters.Add(new CustomEnumConverter());
So lets put thing together:
public enum Foo
{
A = 1,
B = 2,
// what ever name and enum number that fits your logic
Unknown = 99
}
public class Bar
{
public Foo? Foo { get; set; }
}
public static void Main()
{
var options = new JsonSerializerOptions();
options.Converters.Add(new CustomEnumConverter());
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
var jsonString = "{\"foo\": \"C\"}";
var jsonSpan = (ReadOnlySpan<byte>)Encoding.UTF8.GetBytes(jsonString);
try
{
var result = JsonSerializer.Deserialize<Bar>(jsonSpan, options);
if (result.Foo == Foo.Unknown)
result.Foo = null;
Console.WriteLine(result.Foo == null);
}
catch (Exception ex)
{
Console.WriteLine("Serialization Failed" + ex.Message);
}
}
Here is the code CustomEnumConverter
internal sealed class CustomEnumConverter : JsonConverter<Foo>
{
public override Foo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.String:
var isNullable = IsNullableType(typeToConvert);
var enumType = isNullable ? Nullable.GetUnderlyingType(typeToConvert) : typeToConvert;
var names = Enum.GetNames(enumType ?? throw new InvalidOperationException());
if (reader.TokenType != JsonTokenType.String) return Foo.Unknown;
var enumText = System.Text.Encoding.UTF8.GetString(reader.ValueSpan);
if (string.IsNullOrEmpty(enumText)) return Foo.Unknown;
var match = names.FirstOrDefault(e => string.Equals(e, enumText, StringComparison.OrdinalIgnoreCase));
return (Foo) (match != null ? Enum.Parse(enumType, match) : Foo.Unknown);
default:
throw new ArgumentOutOfRangeException();
}
}
public override void Write(Utf8JsonWriter writer, Foo value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
private static bool IsNullableType(Type t)
{
return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>));
}
}
Running this code should return True with out exception.
For this solution I got some inspiration from here.
The other way is a bit similar but using Newtonsoft.
Note: Remember what I did here is just example to demonstrate stuff,
please validate every thing, test it before going production.
Newtonsoft (Original Answer)
Another way to solve this using Newtonsoft with custom JsonConverter.
What you do is added attribute of your custom JsonConverter to your Foo class [JsonConverter(typeof(CustomEnumConverter))].
Then make your class method to return null if the enum is not recognized.
You can of course customize almost any type and have different customization classes.
Ok install Newtonsoft.Json nuget package via Nuget Manager.
We start with you code modification:
//add the attribute here
[JsonConverter(typeof(CustomEnumConverter))]
public enum Foo
{
A = 1,
B = 2
}
public class Bar
{
public Foo? Foo { get; set; }
}
public static void Main()
{
var jsonString = "{\"foo\": \"C\"}";
try
{
// use newtonsoft json converter
var result = JsonConvert.DeserializeObject<Bar>(jsonString);
Console.WriteLine(result.Foo == null);
}
catch (Exception ex)
{
Console.WriteLine("Serialization Failed" + ex.Message);
}
}
And now for your customization class:
public class CustomEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var type = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType;
return type != null && type.IsEnum;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var isNullable = IsNullableType(objectType);
var enumType = isNullable ? Nullable.GetUnderlyingType(objectType) : objectType;
var names = Enum.GetNames(enumType ?? throw new InvalidOperationException());
if (reader.TokenType != JsonToken.String) return null;
var enumText = reader.Value.ToString();
if (string.IsNullOrEmpty(enumText)) return null;
var match = names.FirstOrDefault(e => string.Equals(e, enumText, StringComparison.OrdinalIgnoreCase));
return match != null ? Enum.Parse(enumType, match) : null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override bool CanWrite => true;
private static bool IsNullableType(Type t)
{
return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>));
}
}
Now it is test time.
When we fire the program with out [JsonConverter(typeof(CustomEnumConverter))] we get error as shown here:
But when we added [JsonConverter(typeof(CustomEnumConverter))] and run the program again it works:
Links:
https://www.newtonsoft.com/json
I got inspiration from this answer: How can I ignore unknown enum values during json deserialization?
https://bytefish.de/blog/enums_json_net/
You can deserialize into a string and TryParse
public class Bar
{
public string Foo { get; set; }
public Foo? FooEnum { get; set; }
}
...
var result = JsonSerializer.Deserialize<Bar>(jsonSpan, options);
Enum.TryParse<Foo>(result, out Bar.FooEnum);
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;
}
I've run into a confusing problem that i cannot seem to solve.
I'm using Json.Net and i've written a custom Json converter to handle a special case in my application.
The issue i've run into is in the deserialize or ReadJson method which is throwing an error when it tires to convert a JSON array into an array of strings:
The exact text of the error is: Unexpected token while deserializing object: PropertyName. Path 'RootPages', line 1, position 13.
As you can see from the inspector, the JProperty it is trying to deserialize (RootPages) has been parsed correctly and is valid JSON.
So i'm not entirely sure what is going on here, any enlightenment would be greatly appreciated..
If relevant, the original JSON string is as follows:
{
"RootPages": [
"TestItem1",
"TestItem2"
],
"Name": "root"
}
EDIT-CODE:
ReadJson Method:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
if (PartialChildPageSerialization) {
var jsonObject = JObject.Load(reader);
var properties = jsonObject.Properties().ToList();
foreach (var property in objectType.GetProperties()) {
var type = property.PropertyType;
object value = null;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ChildPageCollection<>)) {
var collection = Activator.CreateInstance(type);
var deserializedValue = properties.First(p => p.Name == property.Name).ToObject<string[]>();
type.GetMethod("PolulateFromSerializer").Invoke(collection, new object[] {deserializedValue});
value = collection;
}
else {
value = properties.First(p => p.Name == property.Name).ToObject(type);
}
property.SetValue(existingValue, value);
}
return existingValue;
}
return serializer.Deserialize(reader, objectType);
}
Snippet of the interesting part of ChildPageCollection:
public class ChildPageCollection<T> : IList<T> where T : DataPage
{
public string[] GetNames() => _internalNameList.ToArray();
internal void PolulateFromSerializer(string[] names) {
this.Clear();
_internalNameList.AddRange(names);
_hasFullList = false;
}
private void CheckFullList() {
if(!_hasFullList)
throw new InvalidOperationException("Collection has not been fully loaded, and full list is not avialable.");
}
private readonly List<T> _internalList = new List<T>();
private readonly List<string> _internalNameList = new List<string>();
private bool _hasFullList = true;
...
}
I expect this is because the first property is an object with a property called 'RootPages' which is a string[].
Unfortunately from the looks of your screenshots you are trying to turn an object into a string array.
This should work in the example you've given:
properties.First(p => p.Name == property.Name).Select(o => o.Children().Values<string>()).First().ToArray();
In place of:
properties.First(p => p.Name == property.Name).ToObject<string[]>();