Reputation: 23135
Consider the following Json.NET
serialization style code:
[JsonConverter(typeof(MyConverter))]
class A {
...
}
[JsonConverter(typeof(MyConverter))]
class B : A {
...
}
class MyConverter {
public override void WriteJson(
JsonWriter writer,
object value,
JsonSerializer serializer) { ... }
}
class CA {
public readonly A _x;
CA(A x) { _x = x; }
}
class CB {
public readonly B _x;
CB(B x) { _x = x; }
}
private static void Main(string[] args) {
B b = new B(...);
CA ca = new CA(b);
CB cb = new CB(b);
string caStr = JsonConvert.SerializeObject(ca);
string cbStr = JsonConvert.SerializeObject(cb);
}
In the code above, both CA
and CB
are serialised to the exact same string caStr
and cbStr
. But I want MyConverter
to know about the compile time type and act differently. In the case of CB
, the contained B _x
should just be serialised in the default manner. By in the case of CA
, MyConverter
should tag what it serialises in some defined way and then serilise the contained A _x
(which has a runtime type of B
) in the usual default way B is serialised.
So, my question is, can a custom JsonConverter
receive information on the compile time type of the type it is serialising? In this case adjusting the serialising call won't do, as I need this to work with member objects also, of which I'm not directly calling to serialise.
Note that whilst what I'm actually doing is similar to TypeNameHandling
it's not exactly the same and I need to conform to an external spec so I really need the custom behaviour here.
Upvotes: 2
Views: 376
Reputation: 116533
You can take advantage of the fact that converters applied to properties take precedence over other converters to use a customized converter instance for A _x
.
As explained in its Serialization Guide:
JsonConverters can be defined and specified in a number of places: in an attribute on a member, in an attribute on a class, and added to the JsonSerializer's converters collection. The priority of which JsonConverter is used is the JsonConverter defined by attribute on a member, then the JsonConverter defined by an attribute on a class, and finally any converters passed to the JsonSerializer.
This makes it possible to pass the declared type information (or any other compile-time information) to MyConverter
when applied to A _x
by using converter parameters. In comments you wrote I need the compile time type revealed to the custom converter so if by compile time type you meant the declared type A
of _x
you may do that as follows.
Modify MyConverter
to add a constructor with a declaredType
parameter:
class MyConverter : JsonConverter
{
public MyConverter() : this(typeof(A)) { }
public MyConverter(Type declaredType) => this.DeclaredType = declaredType;
public Type DeclaredType { get; init; }
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer)
{
// This is just a mockup, your question doesn't specify your requirements.
var a = (A)value;
writer.WriteStartObject();
if (DeclaredType == typeof(A) && a is B b)
{
//By in the case of CA, MyConverter should tag what it serialises in some defined way
writer.WritePropertyName("isB");
writer.WriteValue(true);
}
// Add whatever logic you need to serialize A
// Add whatever additional logic you need to serialize B
// And end the object
writer.WriteEndObject();
}
public override bool CanConvert(Type objectType) => typeof(A).IsAssignableFrom(objectType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
}
Then modify your classes as follows:
class CA {
[JsonConverter(typeof(MyConverter), typeof(A))]
public readonly A _x;
public CA(A _x) { this._x = _x; } // The constructor argument names must have the same case-invariant name as the corresponding member for the converter to be applied
}
class CB {
[JsonConverter(typeof(MyConverter), typeof(B))]
public readonly B _x;
public CB(B _x) { this._x = _x; }
}
[JsonConverter(typeof(MyConverter))]
class A {
}
[JsonConverter(typeof(MyConverter), typeof(B))]
class B : A {
}
Alternatively, you could make MyConverter
generic in the declared type:
class MyConverter<TDeclared> : JsonConverter<TDeclared> where TDeclared : A
{
public override void WriteJson( JsonWriter writer, TDeclared value, JsonSerializer serializer)
{
// This is just a mockup, your question doesn't specify your requirements.
writer.WriteStartObject();
if (typeof(TDeclared) != value.GetType())
{
//By in the case of CA, MyConverter should tag what it serialises in some defined way
writer.WritePropertyName("isB");
writer.WriteValue(value.GetType() == typeof(B));
}
// Add whatever logic you need to serialize A
// Add whatever additional logic you need to serialize B
// And end the object
writer.WriteEndObject();
}
public override TDeclared ReadJson(JsonReader reader, Type objectType, TDeclared existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
}
And apply it as follows:
class CA {
[JsonConverter(typeof(MyConverter<A>))]
public readonly A _x;
public CA(A _x) { this._x = _x; } // The constructor argument names must have the same case-invariant name as the corresponding member for the converter to be applied
}
class CB {
[JsonConverter(typeof(MyConverter<B>))]
public readonly B _x;
public CB(B _x) { this._x = _x; }
}
[JsonConverter(typeof(MyConverter<A>))]
class A {
}
[JsonConverter(typeof(MyConverter<B>))]
class B : A {
}
Either way, CA ca
will be serialized as follows:
{"_x":{"isB":true}}
While CB cb
will still get serialized like so:
{"_x":{}}
Notes:
As Json.NET is a contract-based serializer, it generally doesn't provide information about the current serialization stack when serializing an object. The parent object decides what to serialize, the child object decides how to serialize itself. Thus e.g. MyConverter
isn't informed whether the container object is of type CA
or CB
or what specific property of that container is being written.
Custom member converters are the one notable exception to that general principle.
In both cases my code assumes that you do not want B
tagged when serialized standalone. If you do want it tagged standalone please edit your question to clarify.
When deserializing an immutable type with a parameterized constructor, Json.NET matches argument names to properties using a case-invariant match in order to use find and use [JsonProperty]
or [JsonConverter]
attributes that may be applied to the property. Thus I modified your constructor argument names to _x
to ensure MyConverter
is applied during deserialization.
Demo fiddle #1 here for the parameterized converter, and #2 here for the generic version.
Upvotes: 2
Reputation: 13763
Json.NET has a TypeNameHandling config parameter to control this behavior.
class CA
{
A _x;
CA(A x) { _x = x; }
// Added this just so we can confirm the solution easily
public A X => _x;
}
B b = new B(...);
CA ca = new CA(b);
string ca_json = JsonConvert.SerializeObject(ca, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
var ca_deserialized = JsonConvert.DeserializeObject<CA>(ca_json);
Console.WriteLine(ca_deserialized.X is B); // true
When activated, Json.NET will add a hidden "$type" field to the JSON, which tells the deserializer exactly which type to use. If you inspect the JSON, you'll see something along the lines of:
"$type": "MyNamespace.CA",
Upvotes: 0