(this issue stems from trying to serialize/deserialize LikeType classes to JSON - https://github.com/kleinwareio/LikeType)
I have:
public abstract class LikeType<T>
{
public T Value;
// ....
// how to tell json.net to serialize/deserialize classes deriving
// from this like it would T ???
}
public class Name : LikeType<string> {
public Name(string s) : base(s) { }
// does not add any properties
}
void test()
{
var name = new Name("john");
var jobj = new JObject();
try
{
jobj.Add("key", new JObject(name));
}
catch (Exception e)
{
!Exeption !
e = {System.ArgumentException: Could not determine JSON object type for type Name. at Newtonsoft.Json.Linq.JValue.GetValueType(Nullable`1 current, Object value) at Newtonsoft.Json.Linq.JContainer.CreateFromContent(Object content)
}
}
How can I specify that all classes deriving from LikeType<T> will be serialized/ deserialized to JSON with Json.Net in the same way T would?
(in this case, Json.Net should serialize/deserialize Name in the same way it would a string)
I believe you want to "forward" LikeType<T> serialization, treating this like an invisible wrapper type. This assumption is crucial to my solution.
I'd suggest using JsonConverter implementation to do that. There is a very similar post here: Json.NET - Serialize generic type wrapper without property name
I've adapted the example to your case. This is the adapted approach:
class LikeTypeConverter : JsonConverter
{
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(LikeType<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
public override bool CanConvert(Type objectType)
{
return GetValueType(objectType) != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// You need to decide whether a null JSON token results in a null LikeType<T> or
// an allocated LikeType<T> with a null Value.
if (reader.SkipComments().TokenType == JsonToken.Null)
return null;
var valueType = GetValueType(objectType);
var value = serializer.Deserialize(reader, valueType);
// Here we assume that every subclass of LikeType<T> has a constructor with a single argument, of type T.
return Activator.CreateInstance(objectType, value);
}
const string ValuePropertyName = "Value";// nameof(LikeType<object>.Value); // in C#6+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
var valueProperty = contract.Properties.Single(p => p.UnderlyingName == ValuePropertyName);
serializer.Serialize(writer, valueProperty.ValueProvider.GetValue(value));
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
{
}
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
You can use this as an attribute on LikeType<T> declaration if you want to include this in your library:
[JsonConverter(typeof(LikeTypeConverter))]
public abstract class LikeType<T> { ... }
Or you can use the converter when necessary, modifying JsonSerializerSettings.Converters collection:
var settings = new JsonSerializerSettings
{
Converters = { new LikeTypeConverter() },
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var result = JsonConvert.SerializeObject(myObject, Formatting.Indented, settings);
I've also created a working dotnetfiddle sample for demonstration (also adapting the one from linked post).
One way of controlling what most serializers serialize is using the Serializable attribute and implement the ISerializable interface.
Related
I have a generic type that wraps a single primitive type to give it value equality semantics
public class ValueObject<T>
{
public T Value { get; }
public ValueObject(T value) => Value = value;
// various other equality members etc...
}
It is used like:
public class CustomerId : ValueObject<Guid>
{
public CustomerId(Guid value) : base(value) { }
}
public class EmailAddress : ValueObject<string>
{
public EmailAddress(string value) : base(value) { }
}
The issue is when serializing a type like:
public class Customer
{
public CustomerId Id { get; }
public EmailAddress Email { get; }
public Customer(CustomerId id, EmailAddress email)
{
Id = id;
Email = email;
}
}
Each object the inherits from ValueObject<T> is wrapped in a Value property (as expected). For example
var customerId = new CustomerId(Guid.NewGuid());
var emailAddress = new EmailAddress("some#email.com");
var customer = new Customer(customerId, emailAddress);
var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
})
Results in
{
"id": {
"value": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c"
},
"email": {
"value": "some#email.com"
}
}
Is there a way to write a custom JsonConverter so the the Value property is excluded for types subclassing ValueObject<T> so that the above example would output
{
"id": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c",
"email": "some#email.com"
}
I would prefer to have a single JsonConverter that can handle all ValueObject<T> rather than having to define a separate JsonConverter for each ValueObject<T> subclass
My first attempt was
public class ValueObjectOfTConverter : JsonConverter
{
private static readonly Type ValueObjectGenericType = typeof(ValueObject<>);
private static readonly string ValuePropertyName = nameof(ValueObject<object>.Value);
public override bool CanConvert(Type objectType) =>
IsSubclassOfGenericType(objectType, ValueObjectGenericType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// converts "f5ce21a5-a0d1-4888-8d22-6f484794ac7c" => "value": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c"
var existingJsonWrappedInValueProperty = new JObject(new JProperty(ValuePropertyName, JToken.Load(reader)));
return existingJsonWrappedInValueProperty.ToObject(objectType, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// to implement
}
private static bool IsSubclassOfGenericType(Type typeToCheck, Type openGenericType)
{
while (typeToCheck != null && typeToCheck != typeof(object))
{
var cur = typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck;
if (openGenericType == cur) return true;
typeToCheck = typeToCheck.BaseType;
}
return false;
}
}
You can do this with a custom JsonConverter similar to the ones shown in Json.Net: Serialize/Deserialize property as a value, not as an object. However, since ValueObject<T> does not have a non-generic method to get and set the Value as an object, you will need to use reflection.
Here's one approach:
class ValueConverter : JsonConverter
{
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
public override bool CanConvert(Type objectType)
{
return GetValueType(objectType) != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// You need to decide whether a null JSON token results in a null ValueObject<T> or
// an allocated ValueObject<T> with a null Value.
if (reader.SkipComments().TokenType == JsonToken.Null)
return null;
var valueType = GetValueType(objectType);
var value = serializer.Deserialize(reader, valueType);
// Here we assume that every subclass of ValueObject<T> has a constructor with a single argument, of type T.
return Activator.CreateInstance(objectType, value);
}
const string ValuePropertyName = nameof(ValueObject<object>.Value);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
var valueProperty = contract.Properties.Where(p => p.UnderlyingName == ValuePropertyName).Single();
// You can simplify this to .Single() if ValueObject<T> has no other properties:
// var valueProperty = contract.Properties.Single();
serializer.Serialize(writer, valueProperty.ValueProvider.GetValue(value));
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
You could then apply the converter directly to ValueType<T> like so:
[JsonConverter(typeof(ValueConverter))]
public class ValueObject<T>
{
// Remainder unchanged
}
Or apply it in settings instead:
var settings = new JsonSerializerSettings
{
Converters = { new ValueConverter() },
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, settings);
Working sample .Net fiddle #1 here.
Alternatively, you might consider adding a non-generic method to access the value as an object, e.g. like so:
public interface IHasValue
{
object GetValue(); // A method rather than a property to ensure the non-generic value is never serialized directly.
}
public class ValueObject<T> : IHasValue
{
public T Value { get; }
public ValueObject(T value) => Value = value;
// various other equality members etc...
#region IHasValue Members
object IHasValue.GetValue() => Value;
#endregion
}
With this addition, WriteJson() becomes much simpler:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, ((IHasValue)value).GetValue());
}
Working sample .Net fiddle #2 here.
Notes:
ReadJson() assumes that every subclass of Value<T> has a public constructor taking a single argument of type T.
Applying the converter directly to ValueType<T> using [JsonConverter(typeof(ValueConverter))] will have slightly better performance, since CanConvert need never get called. See Performance Tips: JsonConverters for details.
You need to decide how to handle a null JSON token. Should it result in a null ValueType<T>, or an allocated ValueType<T> with a null Value?
In the second version of ValueType<T> I implemented IHasValue.GetValue() explicitly to discourage its use in cases where an instance of ValueType<T> is used in statically typed code.
If you really only want to apply the converter to types subclassing ValueObject<T> and not ValueObject<T> itself, in GetValueType(Type objectType) add a call to .Skip(1):
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Skip(1) // Do not apply the converter to ValueObject<T> when not subclassed
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
And then apply the converter in JsonSerializerSettings.Converters rather than directly to ValueObject<T>.
public class JsonModel
{
[TypeConverter(typeof(CidNumberConvertor))]
[JsonProperty("cid_number")]
public Cid CidNumber;
[TypeConverter(typeof(CidHexaConvertor))]
[JsonProperty("cid_hexa")]
public Cid CidHexa;
[JsonProperty("cid_default")]
public Cid CidDefault;
}
Imagine I've 3 fields and all are of type Cid. I've globally registered TypeConvertor CidHexaConvertor. It seems TypeConvertor attribute is ignored on attributes itself and is invoked only when define on the class/model itself. CidHexaConvertor has method to convert string to Cid and Cid to string. I can share more code later, but it seems attributes like this are not possible. Any clue?
Checking for [TypeConverter(typeof(...))] attributes applied to members is not implemented out of the box in Json.NET. You could, however, create a custom JsonConverter that wraps an arbitrary TypeConverter, then apply that to your model using JsonConverterAttribute.
First, define the following JsonConverter:
public class TypeConverterJsonConverter : JsonConverter
{
readonly TypeConverter converter;
public TypeConverterJsonConverter(Type typeConverterType) : this((TypeConverter)Activator.CreateInstance(typeConverterType)) { }
public TypeConverterJsonConverter(TypeConverter converter)
{
if (converter == null)
throw new ArgumentNullException();
this.converter = converter;
}
public override bool CanConvert(Type objectType)
{
return converter.CanConvertFrom(typeof(string)) && converter.CanConvertTo(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var tokenType = reader.SkipComments().TokenType;
if (tokenType == JsonToken.Null)
return null;
if (!tokenType.IsPrimitive())
throw new JsonSerializationException(string.Format("Token {0} is not primitive.", tokenType));
var s = (string)JToken.Load(reader);
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var s = converter.ConvertToInvariantString(value);
writer.WriteValue(s);
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
public static bool IsPrimitive(this JsonToken tokenType)
{
switch (tokenType)
{
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
case JsonToken.Boolean:
case JsonToken.Undefined:
case JsonToken.Null:
case JsonToken.Date:
case JsonToken.Bytes:
return true;
default:
return false;
}
}
}
Then apply it to your model as follows:
public class JsonModel
{
[JsonConverter(typeof(TypeConverterJsonConverter), typeof(CidNumberConvertor))]
[TypeConverter(typeof(CidNumberConvertor))]
[JsonProperty("cid_number")]
public Cid CidNumber;
[JsonConverter(typeof(TypeConverterJsonConverter), typeof(CidHexaConvertor))]
[TypeConverter(typeof(CidHexaConvertor))]
[JsonProperty("cid_hexa")]
public Cid CidHexa;
[JsonProperty("cid_default")]
public Cid CidDefault;
}
Notes:
Applying a JsonConverter overrides use of the global default TypeConverter for Cid.
The JsonConverterAttribute(Type,Object[]) constructor is used to pass the specific TypeConverter type to the constructor of TypeConverterJsonConverter as an argument.
In production code, I assume those are properties not fields.
Sample fiddle #1 here. (In the absence of a mcve I had to create a stub implementation of Cid.)
Alternatively, if you have many properties for which you want to use an applied TypeConverter when serializing to JSON, you can create a custom ContractResolver that instantiates and applies TypeConverterJsonConverter automatically:
public class PropertyTypeConverterContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.Converter == null)
{
// Can more than one TypeConverterAttribute be applied to a given member? If so,
// what should we do?
var attr = property.AttributeProvider.GetAttributes(typeof(TypeConverterAttribute), false)
.OfType<TypeConverterAttribute>()
.SingleOrDefault();
if (attr != null)
{
var typeConverterType = GetTypeFromName(attr.ConverterTypeName, member.DeclaringType.Assembly);
if (typeConverterType != null)
{
var jsonConverter = new TypeConverterJsonConverter(typeConverterType);
if (jsonConverter.CanConvert(property.PropertyType))
{
property.Converter = jsonConverter;
// MemberConverter is obsolete or removed in later versions of Json.NET but
// MUST be set identically to Converter in earlier versions.
property.MemberConverter = jsonConverter;
}
}
}
}
return property;
}
static Type GetTypeFromName(string typeName, Assembly declaringAssembly)
{
// Adapted from https://referencesource.microsoft.com/#System/compmod/system/componentmodel/PropertyDescriptor.cs,1c1ca94869d17fff
if (string.IsNullOrEmpty(typeName))
{
return null;
}
Type typeFromGetType = Type.GetType(typeName);
Type typeFromComponent = null;
if (declaringAssembly != null)
{
if ((typeFromGetType == null) ||
(declaringAssembly.FullName.Equals(typeFromGetType.Assembly.FullName)))
{
int comma = typeName.IndexOf(',');
if (comma != -1)
typeName = typeName.Substring(0, comma);
typeFromComponent = declaringAssembly.GetType(typeName);
}
}
return typeFromComponent ?? typeFromGetType;
}
}
Then use it as follows:
// Cache statically for best performance.
var resolver = new PropertyTypeConverterContractResolver();
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
};
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
var root2 = JsonConvert.DeserializeObject<JsonModel>(json, settings);
Notes:
You may want to cache the contract resolver for best performance.
JsonProperty.MemberConverter is obsolete in the current version of Json.NET but must be set identically to JsonProperty.Converter in earlier versions.
Sample fiddle #2 here.
I have a generic type that wraps a single primitive type to give it value equality semantics
public class ValueObject<T>
{
public T Value { get; }
public ValueObject(T value) => Value = value;
// various other equality members etc...
}
It is used like:
public class CustomerId : ValueObject<Guid>
{
public CustomerId(Guid value) : base(value) { }
}
public class EmailAddress : ValueObject<string>
{
public EmailAddress(string value) : base(value) { }
}
The issue is when serializing a type like:
public class Customer
{
public CustomerId Id { get; }
public EmailAddress Email { get; }
public Customer(CustomerId id, EmailAddress email)
{
Id = id;
Email = email;
}
}
Each object the inherits from ValueObject<T> is wrapped in a Value property (as expected). For example
var customerId = new CustomerId(Guid.NewGuid());
var emailAddress = new EmailAddress("some#email.com");
var customer = new Customer(customerId, emailAddress);
var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
})
Results in
{
"id": {
"value": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c"
},
"email": {
"value": "some#email.com"
}
}
Is there a way to write a custom JsonConverter so the the Value property is excluded for types subclassing ValueObject<T> so that the above example would output
{
"id": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c",
"email": "some#email.com"
}
I would prefer to have a single JsonConverter that can handle all ValueObject<T> rather than having to define a separate JsonConverter for each ValueObject<T> subclass
My first attempt was
public class ValueObjectOfTConverter : JsonConverter
{
private static readonly Type ValueObjectGenericType = typeof(ValueObject<>);
private static readonly string ValuePropertyName = nameof(ValueObject<object>.Value);
public override bool CanConvert(Type objectType) =>
IsSubclassOfGenericType(objectType, ValueObjectGenericType);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// converts "f5ce21a5-a0d1-4888-8d22-6f484794ac7c" => "value": "f5ce21a5-a0d1-4888-8d22-6f484794ac7c"
var existingJsonWrappedInValueProperty = new JObject(new JProperty(ValuePropertyName, JToken.Load(reader)));
return existingJsonWrappedInValueProperty.ToObject(objectType, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// to implement
}
private static bool IsSubclassOfGenericType(Type typeToCheck, Type openGenericType)
{
while (typeToCheck != null && typeToCheck != typeof(object))
{
var cur = typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck;
if (openGenericType == cur) return true;
typeToCheck = typeToCheck.BaseType;
}
return false;
}
}
You can do this with a custom JsonConverter similar to the ones shown in Json.Net: Serialize/Deserialize property as a value, not as an object. However, since ValueObject<T> does not have a non-generic method to get and set the Value as an object, you will need to use reflection.
Here's one approach:
class ValueConverter : JsonConverter
{
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
public override bool CanConvert(Type objectType)
{
return GetValueType(objectType) != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// You need to decide whether a null JSON token results in a null ValueObject<T> or
// an allocated ValueObject<T> with a null Value.
if (reader.SkipComments().TokenType == JsonToken.Null)
return null;
var valueType = GetValueType(objectType);
var value = serializer.Deserialize(reader, valueType);
// Here we assume that every subclass of ValueObject<T> has a constructor with a single argument, of type T.
return Activator.CreateInstance(objectType, value);
}
const string ValuePropertyName = nameof(ValueObject<object>.Value);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
var valueProperty = contract.Properties.Where(p => p.UnderlyingName == ValuePropertyName).Single();
// You can simplify this to .Single() if ValueObject<T> has no other properties:
// var valueProperty = contract.Properties.Single();
serializer.Serialize(writer, valueProperty.ValueProvider.GetValue(value));
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
You could then apply the converter directly to ValueType<T> like so:
[JsonConverter(typeof(ValueConverter))]
public class ValueObject<T>
{
// Remainder unchanged
}
Or apply it in settings instead:
var settings = new JsonSerializerSettings
{
Converters = { new ValueConverter() },
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var customerAsJson = JsonConvert.SerializeObject(customer, Formatting.Indented, settings);
Working sample .Net fiddle #1 here.
Alternatively, you might consider adding a non-generic method to access the value as an object, e.g. like so:
public interface IHasValue
{
object GetValue(); // A method rather than a property to ensure the non-generic value is never serialized directly.
}
public class ValueObject<T> : IHasValue
{
public T Value { get; }
public ValueObject(T value) => Value = value;
// various other equality members etc...
#region IHasValue Members
object IHasValue.GetValue() => Value;
#endregion
}
With this addition, WriteJson() becomes much simpler:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, ((IHasValue)value).GetValue());
}
Working sample .Net fiddle #2 here.
Notes:
ReadJson() assumes that every subclass of Value<T> has a public constructor taking a single argument of type T.
Applying the converter directly to ValueType<T> using [JsonConverter(typeof(ValueConverter))] will have slightly better performance, since CanConvert need never get called. See Performance Tips: JsonConverters for details.
You need to decide how to handle a null JSON token. Should it result in a null ValueType<T>, or an allocated ValueType<T> with a null Value?
In the second version of ValueType<T> I implemented IHasValue.GetValue() explicitly to discourage its use in cases where an instance of ValueType<T> is used in statically typed code.
If you really only want to apply the converter to types subclassing ValueObject<T> and not ValueObject<T> itself, in GetValueType(Type objectType) add a call to .Skip(1):
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Skip(1) // Do not apply the converter to ValueObject<T> when not subclassed
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueObject<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
And then apply the converter in JsonSerializerSettings.Converters rather than directly to ValueObject<T>.
Suppose I have a class like this:
public class Example {
public int TypedProperty { get; set; }
public object UntypedProperty { get; set; }
}
And suppose someone comes along and writes:
var example = new Example
{
TypedProperty = 5,
UntypedProperty = Guid.NewGuid()
}
If I serialize this with JsonConvert.SerializeObject(example), I get
{
"TypedProperty": 5,
"UntypedProperty": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}
Ideally, I'd like to get something like this:
{
"TypedProperty": 5,
"UntypedProperty":
{
"$type": "System.Guid,mscorlib",
"$value": "24bd733f-2ade-4374-9db6-3c9f3d97b12c"
}
}
But TypeNameHandling doesn't work in this scenario. How can I (de)serialize an untyped property?
If you serialize your class with TypeNameHandling.All or TypeNameHandling.Auto,
then when the UntypedProperty property would be serialized as a JSON container (either an object or array) Json.NET should correctly serialize and deserialize it by storing type information in the JSON file in a "$type" property. However, in cases where UntypedProperty is serialized as a JSON primitive (a string, number, or Boolean) this doesn't work because, as you have noted, a JSON primitive has no opportunity to include a "$type" property.
The solution is, when serializing a type with a property of type object, to serialize wrappers classes for primitive values that can encapsulate the type information, along the lines of this answer. Here is a custom JSON converter that injects such a wrapper:
public class UntypedToTypedValueConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException("This converter should only be applied directly via ItemConverterType, not added to JsonSerializer.Converters");
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var value = serializer.Deserialize(reader, objectType);
if (value is TypeWrapper)
{
return ((TypeWrapper)value).ObjectValue;
}
return value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (serializer.TypeNameHandling == TypeNameHandling.None)
{
Console.WriteLine("ObjectItemConverter used when serializer.TypeNameHandling == TypeNameHandling.None");
serializer.Serialize(writer, value);
}
// Handle a couple of simple primitive cases where a type wrapper is not needed
else if (value is string)
{
writer.WriteValue((string)value);
}
else if (value is bool)
{
writer.WriteValue((bool)value);
}
else
{
var contract = serializer.ContractResolver.ResolveContract(value.GetType());
if (contract is JsonPrimitiveContract)
{
var wrapper = TypeWrapper.CreateWrapper(value);
serializer.Serialize(writer, wrapper, typeof(object));
}
else
{
serializer.Serialize(writer, value);
}
}
}
}
abstract class TypeWrapper
{
protected TypeWrapper() { }
[JsonIgnore]
public abstract object ObjectValue { get; }
public static TypeWrapper CreateWrapper<T>(T value)
{
if (value == null)
return new TypeWrapper<T>();
var type = value.GetType();
if (type == typeof(T))
return new TypeWrapper<T>(value);
// Return actual type of subclass
return (TypeWrapper)Activator.CreateInstance(typeof(TypeWrapper<>).MakeGenericType(type), value);
}
}
sealed class TypeWrapper<T> : TypeWrapper
{
public TypeWrapper() : base() { }
public TypeWrapper(T value)
: base()
{
this.Value = value;
}
public override object ObjectValue { get { return Value; } }
public T Value { get; set; }
}
Then apply it to your type using [JsonConverter(typeof(UntypedToTypedValueConverter))]:
public class Example
{
public int TypedProperty { get; set; }
[JsonConverter(typeof(UntypedToTypedValueConverter))]
public object UntypedProperty { get; set; }
}
If you cannot modify the Example class in any way to add this attribute (your comment The class isn't mine to change suggests as much) you could inject the converter with a custom contract resolver:
public class UntypedToTypedPropertyContractResolver : DefaultContractResolver
{
readonly UntypedToTypedValueConverter converter = new UntypedToTypedValueConverter();
// As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
// http://www.newtonsoft.com/json/help/html/ContractResolver.htm
// http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
// "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
// See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
static UntypedToTypedPropertyContractResolver instance;
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static UntypedToTypedPropertyContractResolver() { instance = new UntypedToTypedPropertyContractResolver(); }
public static UntypedToTypedPropertyContractResolver Instance { get { return instance; } }
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
foreach (var property in contract.Properties.Concat(contract.CreatorParameters))
{
if (property.PropertyType == typeof(object)
&& property.Converter == null)
{
property.Converter = property.MemberConverter = converter;
}
}
return contract;
}
}
And use it as follows:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
ContractResolver = UntypedToTypedPropertyContractResolver.Instance,
};
var json = JsonConvert.SerializeObject(example, Formatting.Indented, settings);
var example2 = JsonConvert.DeserializeObject<Example>(json, settings);
In both cases the JSON created looks like:
{
"TypedProperty": 5,
"UntypedProperty": {
"$type": "Question38777588.TypeWrapper`1[[System.Guid, mscorlib]], Tile",
"Value": "e2983c59-5ec4-41cc-b3fe-34d9d0a97f22"
}
}
Lookup SerializeWithJsonConverters.htm and ReadingWritingJSON.
Call: JsonConvert.SerializeObject(example, new ObjectConverter());
class ObjectConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Example);
}
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)
{
Example e = (Example)value;
writer.WriteStartObject();
writer.WritePropertyName("TypedProperty");
writer.WriteValue(e.TypedProperty);
writer.WritePropertyName("UntypedProperty");
writer.WriteStartObject();
writer.WritePropertyName("$type");
writer.WriteValue(e.UntypedProperty.GetType().FullName);
writer.WritePropertyName("$value");
writer.WriteValue(e.UntypedProperty.ToString());
writer.WriteEndObject();
writer.WriteEndObject();
}
}
I have a complex type that is to be serialized as a string. I have this working as desired using IXmlSerializable and am now trying to get it to work for JSON messages as well. Currently I get it to serialize via the DataContractSerializer to the following:
<MyXmlRootElement>_value's contents</MyXmlRootElement>
For the JSON output I would like to just receive a JSON string with the _value's contents.
This type implements more than described below to also affect the generated wsdl through a few other attributes that hook into our web services framework. So the generated wsdl/xsd all looks great, it just isn't giving me the desired JSON.
[XmlRoot(ElementName = "MyXmlRootElement", Namespace = "MyNamespace"]
public class ComplexType : IXmlSerializable
{
private string _value;
public ComplexType(string value) { _value = value; }
#region IXmlSerialiable Implementation
public XmlSchema GetSchema() { return null; }
public void ReadXml(XmlReader reader)
{
_value = reader.ReadString();
}
public void WriteXml(XmlWriter writer)
{
writer.WriteString(_value);
}
#endregion
}
I tried implementing ISerializable and this does affect the generated JSON, but it still places it in a complex (object) type, i.e, { "ValueKey": "_value's contents" }. Any idea how I can get it to serialize to a pure string, no curly braces?
The solution was easier than expected. You can register a JsonConverter with GlobalConfiguration. I made an abstract wrapper around JsonConverter as below, the idea for which came from the following SO thread: How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?.
public abstract class CustomJsonConverter<T, TResult> : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load token from stream
var token = JToken.Load(reader);
// Create target object based on token
var target = Create(objectType, token);
var targetType = target.GetType();
if (targetType.IsClass && targetType != typeof(string))
{
// Populate the object properties
var tokenReader = token.CreateReader();
CopySerializerSettings(serializer, tokenReader);
serializer.Populate(token.CreateReader(), target);
}
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
T typedValued = (T)value;
TResult valueToSerialize = Convert(typedValued);
serializer.Serialize(writer, valueToSerialize);
}
public override bool CanConvert(Type objectType)
{
return typeof (T) == objectType;
}
protected virtual T Create(Type type, JObject jObject)
{
// reads the token as an object type
if (typeof(TResult).IsClass && typeof(T) != typeof(string))
{
return Convert(token.ToObject<TResult>());
}
var simpleValue = jObject.Value<TResult>();
return Convert(simpleValue);
}
protected abstract TResult Convert(T value);
protected abstract T Convert(TResult value);
private static void CopySerializerSettings(JsonSerializer serializer, JsonReader reader)
{
reader.Culture = serializer.Culture;
reader.DateFormatString = serializer.DateFormatString;
reader.DateTimeZoneHandling = serializer.DateTimeZoneHandling;
reader.DateParseHandling = serializer.DateParseHandling;
reader.FloatParseHandling = serializer.FloatParseHandling;
}
}
You can then use this to do something like the following
public class DateTimeToStringJsonConverter : CustomJsonConverter<DateTime, string>
{
protected override string Convert(DateTime value)
{
return value.ToString();
}
protected override DateTime Convert(string value)
{
return DateTime.Parse(value);
}
}
And then finally register an instance with GlobalConfiguration in Global.asax.cs
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new DateTimeToStringJsonConverter());
Any thoughts/ideas/opinions? Thanks!
EDIT: 2018.10.16 - Updated code to handle value types as the source type and to copy serializer settings over thanks to the following comment from the original SO answer:
NOTE: This solution is all over the internet, but has a flaw that manifests itself in rare occasions. The new JsonReader created in the ReadJson method does not inherit any of the original reader's configuration values (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, etc...). These values should be copied over before using the new JsonReader in serializer.Populate().
import this namespace System.Web.Script.Serialization;
string SerializeObject()
{
var objs = new List<Test>()
var objSerialized = new JavaScriptSerializer();
return objSerialized .Serialize(objs);
}
I use as example a List but you will use your object instead.