当我为个人项目生成一些 JSON 时,我也遇到了这个问题 - 我有一个递归多态数据模型,我想将其转换为 JSON,但是存在作为根对象的派生类型的子对象在根对象中,序列化程序只是为所有派生类型吐出基类型的属性。我用 JsonConverter 和反射摆弄了几个小时,并想出了一个大锤解决方案,可以满足我的需要。
基本上这样做是手动遍历图中的每个对象并使用默认序列化程序序列化每个不是引用类型的成员,但是在遇到引用类型的实例(不是字符串)时,我会动态生成一个新的 JsonConverter 来处理该类型并将其添加到“转换器”列表中,然后递归地使用该新实例将子对象序列化为其真正的运行时类型。
您或许可以将其作为出发点,找到可以满足您需求的解决方案。
转换器:
/// <summary>
/// Instructs the JsonSerializer to serialize an object as its runtime type and not the type parameter passed into the Write function.
/// </summary>
public class RuntimeTypeJsonConverter<T> : JsonConverter<T>
{
private static readonly Dictionary<Type, PropertyInfo[]> _knownProps = new Dictionary<Type, PropertyInfo[]>(); //cache mapping a Type to its array of public properties to serialize
private static readonly Dictionary<Type, JsonConverter> _knownConverters = new Dictionary<Type, JsonConverter>(); //cache mapping a Type to its respective RuntimeTypeJsonConverter instance that was created to serialize that type.
private static readonly Dictionary<Type, Type> _knownGenerics = new Dictionary<Type, Type>(); //cache mapping a Type to the type of RuntimeTypeJsonConverter generic type definition that was created to serialize that type
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && typeToConvert != typeof(string); //this converter is only meant to work on reference types that are not strings
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var deserialized = JsonSerializer.Deserialize(ref reader, typeToConvert, options); //default read implementation, the focus of this converter is the Write operation
return (T)deserialized;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value is IEnumerable) //if the value is an IEnumerable of any sorts, serialize it as a JSON array. Note that none of the properties of the IEnumerable are written, it is simply iterated over and serializes each object in the IEnumerable
{
WriteIEnumerable(writer, value, options);
}
else if (value != null && value.GetType().IsClass == true) //if the value is a reference type and not null, serialize it as a JSON object.
{
WriteObject(writer, value, ref options);
}
else //otherwise just call the default serializer implementation of this Converter is asked to serialize anything not handled in the other two cases
{
JsonSerializer.Serialize(writer, value);
}
}
/// <summary>
/// Writes the values for an object into the Utf8JsonWriter
/// </summary>
/// <param name="writer">The writer to write to.</param>
/// <param name="value">The value to convert to Json.</param>
/// <param name="options">An object that specifies the serialization options to use.</param>
private void WriteObject(Utf8JsonWriter writer, T value, ref JsonSerializerOptions options)
{
var type = value.GetType();
//get all the public properties that we will be writing out into the object
PropertyInfo[] props = GetPropertyInfos(type);
writer.WriteStartObject();
foreach (var prop in props)
{
var propVal = prop.GetValue(value);
if (propVal == null) continue; //don't include null values in the final graph
writer.WritePropertyName(prop.Name);
var propType = propVal.GetType(); //get the runtime type of the value regardless of what the property info says the PropertyType should be
if (propType.IsClass && propType != typeof(string)) //if the property type is a valid type for this JsonConverter to handle, do some reflection work to get a RuntimeTypeJsonConverter appropriate for the sub-object
{
Type generic = GetGenericConverterType(propType); //get a RuntimeTypeJsonConverter<T> Type appropriate for the sub-object
JsonConverter converter = GetJsonConverter(generic); //get a RuntimeTypeJsonConverter<T> instance appropriate for the sub-object
//look in the options list to see if we don't already have one of these converters in the list of converters in use (we may already have a converter of the same type, but it may not be the same instance as our converter variable above)
var found = false;
foreach (var converterInUse in options.Converters)
{
if (converterInUse.GetType() == generic)
{
found = true;
break;
}
}
if (found == false) //not in use, make a new options object clone and add the new converter to its Converters list (which is immutable once passed into the Serialize method).
{
options = new JsonSerializerOptions(options);
options.Converters.Add(converter);
}
JsonSerializer.Serialize(writer, propVal, propType, options);
}
else //not one of our sub-objects, serialize it like normal
{
JsonSerializer.Serialize(writer, propVal);
}
}
writer.WriteEndObject();
}
/// <summary>
/// Gets or makes RuntimeTypeJsonConverter generic type to wrap the given type parameter.
/// </summary>
/// <param name="propType">The type to get a RuntimeTypeJsonConverter generic type for.</param>
/// <returns></returns>
private Type GetGenericConverterType(Type propType)
{
Type generic = null;
if (_knownGenerics.ContainsKey(propType) == false)
{
generic = typeof(RuntimeTypeJsonConverter<>).MakeGenericType(propType);
_knownGenerics.Add(propType, generic);
}
else
{
generic = _knownGenerics[propType];
}
return generic;
}
/// <summary>
/// Gets or creates the corresponding RuntimeTypeJsonConverter that matches the given generic type defintion.
/// </summary>
/// <param name="genericType">The generic type definition of a RuntimeTypeJsonConverter.</param>
/// <returns></returns>
private JsonConverter GetJsonConverter(Type genericType)
{
JsonConverter converter = null;
if (_knownConverters.ContainsKey(genericType) == false)
{
converter = (JsonConverter)Activator.CreateInstance(genericType);
_knownConverters.Add(genericType, converter);
}
else
{
converter = _knownConverters[genericType];
}
return converter;
}
/// <summary>
/// Gets all the public properties of a Type.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
private PropertyInfo[] GetPropertyInfos(Type t)
{
PropertyInfo[] props = null;
if (_knownProps.ContainsKey(t) == false)
{
props = t.GetProperties();
_knownProps.Add(t, props);
}
else
{
props = _knownProps[t];
}
return props;
}
/// <summary>
/// Writes the values for an object that implements IEnumerable into the Utf8JsonWriter
/// </summary>
/// <param name="writer">The writer to write to.</param>
/// <param name="value">The value to convert to Json.</param>
/// <param name="options">An object that specifies the serialization options to use.</param>
private void WriteIEnumerable(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (object item in value as IEnumerable)
{
if (item == null) //preserving null gaps in the IEnumerable
{
writer.WriteNullValue();
continue;
}
JsonSerializer.Serialize(writer, item, item.GetType(), options);
}
writer.WriteEndArray();
}
}
使用方法:
var cars = new List<Car>
{
new Tesla(),
new Tesla(),
new Tesla()
};
var options = new JsonSerializerOptions();
options.Converters.Add(new RuntimeTypeJsonConverter<object>());
var json = JsonSerializer.Serialize(cars, cars.GetType(), options);