For API purposes I need to ignore some fields based on criteria I receive. Usually I could use [ScriptIgnore] attribute to do this.
But how I can ignore fields dynamically (based on some conditionals)?
Use JsonIgnore attribute available in Newtonsoft.Json package.
then, if you want it to be dynamically conditional, see ShouldSerialize
Assuming you use Json.Net, you can create your own converter for a specific type by creating a class that inherits from JsonConverter.
public class MyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(MyType);
}
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 objectToSerialize = new {}; //create the object you want to serialize here, based on your dynamic conditions
new JsonSerializer().Serialize(writer, objectToSerialize); //serialize the object to the current writer
}
}
Then you call JsonConvert.DeserializeObject and pass it your custom converter:
JsonConvert.DeserializeObject<MyType>(jsonString, new MyJsonConverter());
Related
I need to apply a custom converter conditionally based on the depth of the reader. The root of the json object is a Def class that should deserialize like normal, however any Defs within the object should be resolved to a reference to that deserialized Def. My plan is to check the depth of the reader, and if we're not at the root, then create a skeleton Def and add it to a list to be resolved later once we've deserialized all the Defs.
public class DefConverter : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType == typeof(Def);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
if (reader.Depth == 0) {
// Use the default serializer to read the Def
return serializer.Deserialize(reader, objectType); // ?
}
// Create skeleton Def with id
// Add to list of defs to be resolved later
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}
The issue I'm running into is that there doesn't seem to be a way to call the Json.NET default converter, using serializer.Deserialize(reader, objectType) will just cause an infinite loop as it just calls the custom converter.
I managed to get it working using the solution here:
Json.NET Recursive Serialisation: Custom converter attribute seems to be being ignored
Toggling the converter off and on using the CanRead getter
I want to layer multiple JSON converters for one property because I need to specify a converter for the property's inner type that is generic.
Use case:
public class A {
public Option<DateTime> Time { get; set; }
}
I've got an OptionJsonConverter that can deserialize any Option and I want to specify a custom date format string for this property using the DateFormatConverter from this answer.
Writing a custom converter would be a solution, but it's not ideal as I'll have huge code duplication.
I could use a nullable type, but I've already committed my code base to Options in an effort to avoid null comparisons, and this issue may arise for other types in the future anyway.
Converters can modify serializer's Converters property during ReadJson and WriteJson invocation and the new collection's contents are honored during nested serializations and deserializations.
With this, we can make a converter that temporarily adds specified converters to the Converters property like so:
public abstract class CascadeJsonConverterBase : JsonConverter
{
private readonly JsonConverter[] augmentConverters;
protected CascadeJsonConverterBase() : this(new JsonConverter[0]) { }
// this constructor is intended for use with JsonConverterAttribute
protected CascadeJsonConverterBase(object[] augmentConverters)
: this(augmentConverters.Select(FromAttributeData).ToArray())
{ }
protected CascadeJsonConverterBase(JsonConverter[] augmentConverters)
{
this.augmentConverters = augmentConverters;
}
protected static JsonConverter FromAttributeData(object augmentConverterObj)
{
if (!(augmentConverterObj is object[] augmentConverter))
{
throw new ArgumentException($"Each augment converter data should be an object array", nameof(augmentConverters));
}
if (augmentConverter.Length < 1)
{
throw new ArgumentException($"Augment converter data should include at least one item", nameof(augmentConverters));
}
object augmentConverterType = augmentConverter[0];
if (!(augmentConverterType is Type convType))
{
throw new ArgumentException($"Augment converter data should start with its type", nameof(augmentConverters));
}
if (!typeof(JsonConverter).IsAssignableFrom(convType))
{
throw new ArgumentException($"Augment converter type should inherit from JsonConverter abstract type", nameof(augmentConverters));
}
object converter = Activator.CreateInstance(convType, augmentConverter.SubArray(1, augmentConverter.Length - 1));
return (JsonConverter)converter;
}
protected abstract void WriteJsonInner(JsonWriter writer, object value, JsonSerializer serializer);
protected abstract object ReadJsonInner(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
using (AugmentedConverterScope(serializer))
{
WriteJsonInner(writer, value, serializer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
using (AugmentedConverterScope(serializer))
{
return ReadJsonInner(reader, objectType, existingValue, serializer);
}
}
private AugmentedConverterScopeMgr AugmentedConverterScope(JsonSerializer serializer)
{
// add augmented converters
for (int i = augmentConverters.Length - 1; i >= 0; i--)
{
serializer.Converters.Insert(0, augmentConverters[i]);
}
return new AugmentedConverterScopeMgr(serializer, augmentConverters.Length);
}
private class AugmentedConverterScopeMgr : IDisposable
{
private readonly JsonSerializer serializer;
private readonly int converterCount;
public AugmentedConverterScopeMgr(JsonSerializer serializer, int converterCount)
{
this.serializer = serializer;
this.converterCount = converterCount;
}
public void Dispose()
{
// remove augmented converters
for (int i = 0; i < converterCount; i++)
{
serializer.Converters.RemoveAt(0);
}
}
}
}
And then create a converter that wraps another converter's logic like so:
public class CascadeJsonConverter : CascadeJsonConverterBase
{
private readonly JsonConverter wrappedConverter;
public CascadeJsonConverter(Type wrappedConverterType, object[] wrappedConvConstructorArgs, object[] augmentConverters)
: this(CreateConverter(wrappedConverterType, wrappedConvConstructorArgs), augmentConverters.Select(FromAttributeData).ToArray())
{ }
public CascadeJsonConverter(JsonConverter wrappedConverter, JsonConverter[] augmentConverters)
: base(augmentConverters)
{
this.wrappedConverter = wrappedConverter;
}
private static JsonConverter CreateConverter(Type converterType, object[] convConstructorArgs)
{
if (!typeof(JsonConverter).IsAssignableFrom(converterType))
{
throw new ArgumentException($"Converter type should inherit from JsonConverter abstract type", nameof(converterType));
}
return (JsonConverter) Activator.CreateInstance(converterType, convConstructorArgs);
}
public override bool CanConvert(Type objectType)
{
return wrappedConverter.CanConvert(objectType);
}
protected override void WriteJsonInner(JsonWriter writer, object value, JsonSerializer serializer)
{
wrappedConverter.WriteJson(writer, value, serializer);
}
protected override object ReadJsonInner(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return wrappedConverter.ReadJson(reader, objectType, existingValue, serializer);
}
}
which can then be used to accomplish the goal in question like so
public class A {
[JsonConverter(typeof(CascadeJsonConverter), // cascading converter
typeof(OptionJsonConverter), new object[0], // converter definition for the top-level type of the property
new object[] { // collection of converter definitions to use while deserializing the contents of the property
new object[] { typeof(DateFormatConverter), "yyyy'-'MM'-'dd'T'mm':'HH':'FF.ssK" }
})]
public Option<DateTime> Time { get; set; }
}
With this, you can not only use different controllers for generic fields, but also in cases where a class needs to change the converter for some sub-property of a property's class. Neat :)
One caveat for this is that the top-level converter has to use the serializer argument in ReadJson and WriteJson methods to read and write inner values instead of using JToken.Load(reader).ToObject<T>() and JToken.FromObject(x).WriteTo(writer). Otherwise the inner values are read and written using unconfigured serializers.
If there is a nicer way to accomplish the same task, I'd really apprecite you sharing it!
I have a generic type Container<IContentType> where interface IContentType can be one of four concrete ContentX types.
I serialize and everything is fine.
When deserializing using Newtonsoft I use custom type converters and var model = JsonConvert.DeserializeObject<Container<ContentA>>(json, settings) works. The debugger shows I have an Container<ContentA> object.
My plan was when deserializing to attempt a deserialization for each of the four possible ContentX types and catch an exception silently until I "guess" the right one.
However, if I do this within a method like so:
public static Container<IContentType> Deserialize(jsonfile)
{
...
var model = JsonConvert.DeserializeObject<Container<ContentA>>(json, settings)
return model;
}
I get "Cannot implicitly convert Container<ContentA> to Container<IContentType>". ContentA implements IContentType.
Is there a way I can create a cast operator, conversion, dynamic or make the implicit conversion work?
Rather than trying to deserialize as a Container<ContentX> for concrete type(s) X, you should deserialize as a Container<IContentType> using a custom JsonConverter that pre-load the JSON into a JToken and infers the concrete type along the lines of How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects? or Deserializing polymorphic json classes without type information using json.net or Json.Net Serialization of Type with Polymorphic Child Object.
Thus your converter would look something like:
public class ContentConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IContentType);
}
Type GetConcreteType(JObject obj)
{
if (obj.GetValue(nameof(ContentA.SomePropertyOfContentA), StringComparison.OrdinalIgnoreCase) != null)
return typeof(ContentA);
// Add other tests for other content types.
// Return a default type or throw an exception if a unique type cannot be found.
throw new JsonSerializationException("Cannot determine concrete type for IContentType");
}
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 concreteType = GetConcreteType(obj);
return obj.ToObject(concreteType, serializer);
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
And your call to JsonConvert would look like:
var settings = new JsonSerializerSettings
{
Converters = { new ContentConverter() },
};
var model = JsonConvert.DeserializeObject<Container<IContentType>>(json, settings);
Finally, you might be able to choose the type entirely automatically using
new JsonDerivedTypeConverer<IContentType>(typeof(ContentA), typeof(ContentB), typeof(ContentC), typeof(ContentD))
Where JsonDerivedTypeConverer<T> is taken from JsonConverter with Interface.
I am trying to implement a custom JsonConverter for a struct, but I'm having a hard time getting it to work. I have previously implemented a custom converter for another class and that one works flawlessly, I thought I did the same thing for this one but the ReadJson method of my converter is never called.
This is the class:
public class TransformMatrixConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(TransformMatrix).IsAssignableFrom(objectType);
}
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();
}
}
And below is how I use it in my Item class. You can see my other converter, which works fine.
public static Item FromJSON(string json)
{
JsonConverter[] converters = { new LineConverter(), new TransformMatrixConverter() };
return JsonConvert.DeserializeObject<Item>(json, new JsonSerializerSettings()
{
Converters = converters
});
}
What happens: the CanConvert method of my converter is called and correctly returns true when appropriate; however, the ReadJson method is never hit, I have a breakpoint there and also that exception is never thrown. I have verified that the CanRead property of the converter is true. I am at a loss here, any ideas?
I think I solved it with the help of Brian, the problem was unrelated to the code above - the TransformMatrix I thought I was deserializing was just a read-only property. The solution I used was to expose another property which the deserializer can write.
I have written a custom JsonConverter which I am hoping will allow me to serialize and deserialize Encoding objects within my classes:
public class EncodingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsSubclassOf(typeof(Encoding));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((Encoding)value).EncodingName);
}
public override bool CanRead { get { return true; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var name = reader.ReadAsString();
return Encoding.GetEncoding(name);
}
}
However, when I run the following test code, I get an exception when calling DeserializeObject, and the ReadJson method never gets called.
class Program
{
private static void Main(string[] args)
{
var test = new TestClass();
var jsonSettings = new JsonSerializerSettings
{
Converters = new[] { new EncodingConverter(), }
};
var json = JsonConvert.SerializeObject(test, jsonSettings);
var test2 = JsonConvert.DeserializeObject<TestClass>(json, jsonSettings);
}
}
class TestClass
{
public string Property1;
public Encoding Encoding = Encoding.UTF8;
}
The exception message is:
Target type System.Text.Encoding is not a value type or a non-abstract class.
Am I missing something?
There are three problems with your converter that I see.
You are using the wrong check in CanConvert().
You are using the wrong name for the Encoding when serializing.
You are using the wrong method to get the value from the reader when deserializing.
Let's take these one at a time.
First, in your CanConvert method you are using objectType.IsSubclassOf(typeof(Encoding)) to determine whether the converter should handle the Encoding. This works fine on serialization because you have a concrete instance of the encoding (e.g. UTF8Encoding), which is indeed a subclass of Encoding. However, on deserialization, the deserializer doesn't know what concrete type of encoding you are going to make, so the type that is passed to the converter is just Encoding. Since Encoding is not a subclass of itself, CanConvert returns false, and your ReadJson method never gets called. That leaves Json.Net to try to instantiate the Encoding itself, which it can't do (because Encoding is abstract), so it throws the error you mentioned in your question. You should instead use typeof(Encoding).IsAssignableFrom(objectType) inside your CanConvert method.
Second, when serializing the Encoding inside WriteJson, you are outputting the EncodingName property, which is the human-readable display name of the encoding, not the code page name. If you look at the documentation for the Encoding.GetEncoding(string) method, it says:
Parameters
name
Type: System.String
The code page name of the preferred encoding. Any value returned by the WebName property is valid. Possible values are listed in the Name column of the table that appears in the Encoding class topic.
So, you should be outputting the value of the WebName property in your WriteJson method if you want to be able to use this value to later reconstruct the Encoding in ReadJson.
Third, in your ReadJson method you are using reader.ReadAsString() to attempt to get the encoding name from the JSON. This will not work as you expect. When ReadJson is called by Json.Net, the reader is already positioned at the current value. When you call ReadAsString(), that advances the reader to the next token and then attempts to interpret that token as a string. What you really want to do is just get the value of the current token, which you can do using the Value property. Because Value is of type object, you will need to cast it to a string.
Here is the corrected code for the converter:
public class EncodingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Encoding).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((Encoding)value).WebName);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return Encoding.GetEncoding((string)reader.Value);
}
}
Fiddle: https://dotnetfiddle.net/UmLynX
Try:
public class CustomConverter : JsonConverter
{
public override bool CanConvert(System.Type objectType)
{
return true;// objectType.IsAssignableFrom(typeof(Encoding));
}
public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer)
{
return Encoding.GetEncoding(Convert.ToString(reader.Value));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var t = (Test)value;
var e = (Encoding)t.MyProperty;
writer.WriteValue(e.BodyName);
//serializer.Serialize(writer, e.BodyName);
}
}
And in Main:
var o = new Test { MyProperty = Encoding.UTF8 };
var s = new JsonSerializerSettings
{
Converters = new[] { new CustomConverter() }
};
var v = JsonConvert.SerializeObject(o, s);
var o2 = new Test();
o2.MyProperty = Encoding.GetEncoding(JsonConvert.DeserializeObject(v, s).ToString());