Getting FieldInfo within custom JsonConverter - c#

Is there a way to get a reference to the FieldInfo in ReadJson within a custom JsonConverter?
I'm loading a list of Defs from json, however the catch is that the Defs can contain references to other Defs within them. If I can access the FieldInfo then I can add it to a list of fields to be resolved later once all the Defs have loaded.
From what I understand, I'm not able to use Json.NETs PreserveReferencesHandling because I'm loading the json from different files using a several data.ToObject(type, serializer) calls.
Current JsonConverter implementation:
public class DefConverter : JsonConverter {
private bool skipOverMe;
public override bool CanConvert(Type objectType) {
return objectType.IsAssignableTo(typeof(Def));
}
public override bool CanRead {
get {
if (!skipOverMe) return true;
skipOverMe = false;
return false;
}
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
// Skip over root level Defs so they can be deserialized normally
if (reader.Depth == 0) {
skipOverMe = true;
return serializer.Deserialize(reader, objectType);
}
var def = Activator.CreateInstance(objectType) as Def;
def.Id = reader.Value as string;
// Some way to get the FieldInfo so that I can store it in a list to be resolved later
return def;
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}
Example Json file:
{
"class": "ObjectDef",
"id": "ExampleObjectDef",
"name": "example object def",
"someVariable": 5,
"components": {
"ExampleComponent": {
"someDef": "SomeOtherDef"
}
}
}
Alternatively is there some other way I can achieve this def resolution without using reflection?

Related

How can I choose what type to deserialize at runtime based on the structure of the json?

I have some data stored as Json. One property in the data is either an integer (legacy data) like so:
"Difficulty": 2,
Or a complete object (new versions):
"Difficulty": {
"$id": "625",
"CombatModifier": 2,
"Name": "Normal",
"StartingFunds": {
"$id": "626",
"Value": 2000.0
},
"Dwarves": [
"Miner",
"Miner",
"Miner",
"Crafter"
]
},
I am trying to write a custom converter for the type that allows deserialization of both versions.
This is C#, using the latest version of newtonsoft.json.
I've written a converter, and deserializing the integer format is trivial - it's only the mix that is causing me trouble. The only way I can think to check is to try and fail; but this appears to leave the reader in an unrecoverable state. Also, calling deserialize in the catch block leads to an infinite loop.
public class DifficultyConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
var jObject = serializer.Deserialize<JValue>(reader);
if (jObject.Value is Int32 intv)
return Library.EnumerateDifficulties().FirstOrDefault(d => d.CombatModifier == intv);
else
return null;
}
catch (Exception e)
{
return serializer.Deserialize<Difficulty>(reader);
}
}
public override bool CanWrite
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Difficulty);
}
}
Ideally I would be able to serialize into the new format always, and still support reading both formats. A couple of other options include:
Creating another serializer object that does not include the custom converter and calling it from the catch block.
Detecting out of date files at load and modifying the text before attempting to deserialize.
Kind of want to avoid those tho.
You have a couple of problems here:
You are getting an infinite recursion in calls to ReadJson() because your converter is registered with the serializer you are using to do the nested deserialization, either through settings or by directly applying [JsonConverter(typeof(DifficultyConverter))] to Difficulty.
The standard solution to avoid this is to manually allocate your Difficulty and then use serializer.Populate() to deserialize its members (e.g. as shown in this answer to Json.NET custom serialization with JsonConverter - how to get the "default" behavior) -- but you are also using PreserveReferencesHandling.Objects, which does not work with this approach.
What does work with reference preservation is to adopt the approach from this answer to JSON.Net throws StackOverflowException when using [JsonConvert()] and deserialize to some DTO that contains a property of type Difficulty which has a superseding converter applied directly to the property.
serializer.Deserialize<JValue>(reader); may advance the reader past the current token. This will cause the later attempt to deserialize as an object to fail.
Instead, just check the JsonReader.TokenType or preload into a JToken and check the Type.
Putting the above together, your converter should look like the following:
public class DifficultyConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
switch (token.Type)
{
case JTokenType.Null:
return null;
case JTokenType.Integer:
{
var intv = (int)token;
return Library.EnumerateDifficulties().FirstOrDefault(d => d.CombatModifier == intv);
}
case JTokenType.Object:
return token.DefaultToObject(objectType, serializer);
default:
throw new JsonSerializationException(string.Format("Unknown token {0}", token.Type));
}
}
public override bool CanWrite => false;
public override bool CanConvert(Type objectType) => objectType == typeof(Difficulty);
}
Using the following extension methods:
public static partial class JsonExtensions
{
public static object DefaultToObject(this JToken token, Type type, JsonSerializer serializer = null)
{
var oldParent = token.Parent;
var dtoToken = new JObject(new JProperty(nameof(DefaultSerializationDTO<object>.Value), token));
var dtoType = typeof(DefaultSerializationDTO<>).MakeGenericType(type);
var dto = (IHasValue)(serializer ?? JsonSerializer.CreateDefault()).Deserialize(dtoToken.CreateReader(), dtoType);
if (oldParent == null)
token.RemoveFromLowestPossibleParent();
return dto == null ? null : dto.GetValue();
}
public static JToken RemoveFromLowestPossibleParent(this JToken node)
{
if (node == null)
return null;
// If the parent is a JProperty, remove that instead of the token itself.
var contained = node.Parent is JProperty ? node.Parent : node;
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (contained is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}
interface IHasValue
{
object GetValue();
}
[JsonObject(NamingStrategyType = typeof(Newtonsoft.Json.Serialization.DefaultNamingStrategy), IsReference = false)]
class DefaultSerializationDTO<T> : IHasValue
{
public DefaultSerializationDTO(T value) { this.Value = value; }
public DefaultSerializationDTO() { }
[JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
public T Value { get; set; }
public object GetValue() => Value;
}
}
public class NoConverter : JsonConverter
{
// NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
// To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
// By https://stackoverflow.com/users/3744182/dbc
public override bool CanConvert(Type objectType) { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); }
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}
Demo fiddle here.

Edit Json property then deserialize with json.net [duplicate]

Assuming a json string like the following:
string json = '{"string_property":"foo_bar", ... other objects here ...}';
I was wondering if there's a way to run a transformation on the parsed object so that instead of getting foo_bar, I'll get foo bar after running the following method (can be anything really)
public string Transform(string s) {
return s.Replace("_"," ");
}
I can manually alter my poco after deserializing, but wondered what would be a "cleaner" approach?
You can transform your string properties as you deserialize your root object by using a custom JsonConverter targeted at all string type values:
public class ReplacingStringConverter : JsonConverter
{
readonly string oldValue;
readonly string newValue;
public ReplacingStringConverter(string oldValue, string newValue)
{
if (string.IsNullOrEmpty(oldValue))
throw new ArgumentException("string.IsNullOrEmpty(oldValue)");
if (newValue == null)
throw new ArgumentNullException("newValue");
this.oldValue = oldValue;
this.newValue = newValue;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var s = (string)JToken.Load(reader);
return s.Replace(oldValue, newValue);
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then use it like:
var settings = new JsonSerializerSettings { Converters = new[] { new ReplacingStringConverter("_", "") } };
var result = JsonConvert.DeserializeObject<RootObject>(json, settings);
Note however that if individual string-type properties have their own converters applied directly with [JsonConverter(Type)], those converters will be used in preference to the ReplacingStringConverter in the Converters list.
I've ended up doing the following:
First, create a converter that only reads and all it does is url decode the string.
public class UrlDecoderConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var s = (string)JToken.Load(reader);
return HttpUtility.UrlDecode(s);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then, simply add the following to the POCO properties that need to be decoded:
[JsonConverter(typeof(UrlDecoderConverter))]
public string url { get; set; }

How to call JsonConvert.DeserializeObject and disable a JsonConverter applied to a base type via [JsonConverter]?

EDIT: Clarify question:
I have overridden the JsonConverter for a base type (by applying [JsonConverter(typeof(TConverter))] to the superclass), but when deserializing the sub-type directly I want to use STANDARD serialization (i.e. no custom converter) for deserializing my derived object. How do I specify STANDARD serialization for use in the deserialize method, as if I had NOT overridden the JsonConverter?
I am using elastic search and can't call JsonConvert.DeserializeObject with my custom implementation of JsonConverter, and have to rely on the attribute for Elastic to use my converter.
However, using this converter as attribute seems to affect all sub classes as well, but I just want them to use the standard converter, so that I don't have to implement JsonConverter for each of many implementations.
This is my classes/logic as I would like it to look:
[Route("test")]
[HttpPost]
public HttpResponseMessage Test([FromBody] JToken json)
{
var res = json.ToObject<Product>(); // I want an object of ProductImpl type here
return Request.CreateResponse(res);
}
[JsonConverter(typeof(JsonProductConverted))]
public abstract class Product
{
}
public class ProductImpl : Product
{
}
public class JsonProductConverted : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject json = JObject.Load(reader);
//var type = GetTypeFromId((int) json["typeId"]); // Construct type from field in
var type = typeof(ProductImpl);
// var res = JsonConvert.DeserializeObject(json.ToString(), type, DEFAULT_JSONCONVERTER_HERE);
var res = DeserializeToObjectWithStandardJsonConverter(json, type);
return res;
}
public override bool CanConvert(Type objectType)
{
return false;
}
}
If I don't supply the default JsonConverter, or similar it will just use the JsonProductConverted converter, which creates an infinite loop.
Since you have added [JsonConverter(typeof(JsonProductConverted))] directly to your Product type, you could add a dummy converter to ProductImpl that returns false from CanRead and CanWrite:
[JsonConverter(typeof(NoConverter))]
public class ProductImpl : Product
{
}
public class NoConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return false;
}
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
This overrides the base class's converter and then falls back on default serialization for both reading and writing
Sample .Net fiddle.
Another option would be to use serializer.Populate(). This avoids the call to the converter for the object itself:
public class JsonProductConverted : JsonTypeInferringConverterBase
{
protected override Type InferType(Type objectType, JObject json)
{
//var type = GetTypeFromId((int) json["typeId"]); // Construct type from field in
return typeof(ProductImpl);
}
public override bool CanConvert(Type objectType)
{
return false;
}
}
public abstract class JsonTypeInferringConverterBase : JsonConverter
{
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
protected abstract Type InferType(Type objectType, JObject json);
protected virtual object CreateObject(Type actualType, JsonSerializer serializer, JObject json)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(actualType);
return contract.DefaultCreator();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var json = JObject.Load(reader);
var actualType = InferType(objectType, json);
// Construct object (or reuse existingValue if compatible)
if (existingValue == null || !actualType.IsAssignableFrom(existingValue.GetType()))
{
existingValue = CreateObject(actualType, serializer, json);
}
// Populate object.
using (var subReader = json.CreateReader())
{
serializer.Populate(subReader, existingValue);
}
return existingValue;
}
}
Note that the concrete objects must have parameterless constructors for this to work. If not, you can override protected virtual object CreateObject(Type actualType, JsonSerializer serializer, JObject json) and manually invoke a parameterized constructor by deserializing select properties inside the JObject json.
Sample fiddle #2.

intercept JSON in C# to return a list of nullable boolean

I am getting an error when I try to return a list of nullable boolean from my JSON interceptor.
The interceptor attribute is:
[JsonConverter(typeof(NullableBoolListDeSerializer))]
public List<bool?> ExemptBenefits { get; set; }
The ReadJSON method on the interceptor is:
List<bool?> result = new List<bool?>();
(reader as Newtonsoft.Json.Linq.JTokenReader).CurrentToken.ToList().ForEach(item =>
{
string value = (String)item.ToObject(typeof(String));
switch (value.ToLower())
{
case "true":
case "yes":
case "1":
result.Add(true);
break;
case "false":
case "no":
case "0":
default:
result.Add(false);
break;
}
});
return result;
The JSON being submitted is:
{
"exemptBenefits": [
"1"
],
"_apiEndpoint": "/benefits/loan"
}
The error I am getting is:
Unexpected token when deserializing object: String. Path 'exemptBenefits[0]', line 1, position 187.
Wondering how to convert a list of strings (eg "1","0"."true", "false") from JSON to a List (true,false,true,false) in a JSON interceptor
(actually it is NewtonSoft.Json)
If you want to convert a list of string values into a list of nullable boolean values with a JsonConverter class, I would recommend using a JArray inside the converter instead of trying to deal with the reader directly. This will allow you to simplify your code while also avoiding the error you encountered:
class NullableBoolListDeSerializer : JsonConverter
{
readonly string[] TrueStrings = { "true", "yes", "1" };
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<bool?>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return JArray.Load(reader)
.Children<JValue>()
.Select(jv =>
{
string b = (string)jv;
return b != null ? TrueStrings.Contains(b.ToLower()) : (bool?)null;
})
.ToList();
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Fiddle: https://dotnetfiddle.net/JaP5W7
Of course, you can actually do better than that. Instead of making your converter handle a List<bool?>, make it handle just a simple bool? instead, e.g.:
class NullableBoolDeSerializer : JsonConverter
{
readonly string[] TrueStrings = { "true", "yes", "1" };
public override bool CanConvert(Type objectType)
{
return objectType == typeof(bool?);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string b = (string)reader.Value;
return b != null ? TrueStrings.Contains(b.ToLower()) : (bool?)null;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then, swap out the [JsonConverter] attribute on your ExemptBenefits property declaration for a [JsonProperty] attribute that has its ItemConverterType parameter set to the simplified converter. Json.Net will then handle creating the list for you.
[JsonProperty(ItemConverterType = typeof(NullableBoolDeSerializer))]
public List<bool?> ExemptBenefits { get; set; }
Fiddle: https://dotnetfiddle.net/Dp4N11

Json.Net Is converting on its own before using my JsonConverter

In my WPF code, I'm using Newtonsoft.Json to deserialize json into my models. First, I receive a Json string ('json') which I then parse into 'message'. (The object I want to deserialize is wrapped in a "data" field in the json string).
Activity message = JObject.Parse(json)["data"].ToObject<Activity>();
My Activity class uses several [JsonProperty] attributes to generate its fields. One of them is an enum called 'ActivityType'.
[JsonProperty("type")]
[JsonConverter(typeof(ActivityTypeConverter))]
public ActivityType Type { get; set; }
public enum ActivityType {
EmailOpen,
LinkClick,
Salesforce,
Unsupported
};
public class ActivityTypeConverter : JsonConverter {
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var rawString = existingValue.ToString().ToLower();
if (rawString.Contains("click"))
return ActivityType.LinkClick;
else if (rawString.Contains("salesforce"))
return ActivityType.Salesforce;
else if (rawString.Contains("email_open"))
return ActivityType.EmailOpen;
else
{
Console.WriteLine("unsupported " + rawString);
return ActivityType.Unsupported;
}
}
public override bool CanConvert(Type objectType)
{
return !objectType.Equals(typeof(ActivityType));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
What's bizarre and frustrating is that json objects which I know have "type":"email_open" are being deserialized as ActivityType.Unsupported, even though my converter should be deserializing them as EmailOpen.
Debugging has shown what the problem is:
the json field "type" is automatically deserializing "email_open" as EmailOpen and then it is sent through my converter. (It breaks then because my conditional checks for an underscore, while EmailOpen.ToString() doesn't have one.)
So my question then is: Why is it converting without my converter and how do I stop it? I just want it to only use my converter
I think your converter is being called -- it's just not working. The problem is that, rather than reading the new value from the JsonReader reader, you are using the value from the existingValue. But this second value is the pre-existing property value in the class being deserialized, not the value being read.
You need to load the value from the reader along the lines of Json.NET's StringEnumConverter. Here's a version that does that and also handles standard values of your enum by subclassing StringEnumConverter and passing the value read from the file to the base class for further processing:
public class ActivityTypeConverter : StringEnumConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
bool isNullable = (Nullable.GetUnderlyingType(objectType) != null);
Type type = (Nullable.GetUnderlyingType(objectType) ?? objectType);
if (reader.TokenType == JsonToken.Null)
{
if (!isNullable)
throw new JsonSerializationException();
return null;
}
var token = JToken.Load(reader);
if (token.Type == JTokenType.String)
{
var rawString = ((string)token).ToLower();
if (rawString.Contains("click"))
return ActivityType.LinkClick;
else if (rawString.Contains("salesforce"))
return ActivityType.Salesforce;
else if (rawString.Contains("email_open"))
return ActivityType.EmailOpen;
}
using (var subReader = token.CreateReader())
{
while (subReader.TokenType == JsonToken.None)
subReader.Read();
try
{
return base.ReadJson(subReader, objectType, existingValue, serializer); // Use base class to convert
}
catch (Exception ex)
{
return ActivityType.Unsupported;
}
}
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ActivityType);
}
}

Categories