.Net Core 3.0 JsonSerializer 填充现有 object
.Net Core 3.0 JsonSerializer populate existing object
我正在准备从 ASP.NET Core 2.2 迁移到 3.0。
因为我不使用更高级的 JSON 功能(但可能如下所述),并且 3.0 现在为 JSON 提供了 built-in namespace/classes ,System.Text.Json
,我决定看看能不能去掉之前的默认Newtonsoft.Json
.
请注意,我知道 System.Text.Json
不会完全取代 Newtonsoft.Json
。
我到处都做到了,例如
var obj = JsonSerializer.Parse<T>(jsonstring);
var jsonstring = JsonSerializer.ToString(obj);
但在一个地方,我填充了现有的 object。
有了Newtonsoft.Json
一个人就可以做到
JsonConvert.PopulateObject(jsonstring, obj);
built-in System.Text.Json
命名空间有一些额外的 类,比如 JsonDocumnet
、JsonElement
和 Utf8JsonReader
,虽然我不能找到任何将现有 object 作为参数的。
我也没有足够的经验来了解如何利用现有的。
link 可能有 a possible upcoming feature in .Net Core (thanks to Mustafa Gursel),但与此同时(如果没有怎么办)...
...我现在想知道,是否有可能实现与 PopulateObject
类似的东西?
我的意思是,是否有可能与其他 System.Text.Json
类 中的任何一个一起完成相同的任务,而 update/replace 仅设置属性?,...或其他一些聪明的解决方法?
这是我正在寻找的示例 input/output,它需要是通用的,因为传递给反序列化方法的 object 是 <T>
类型)。我有 2 个 Json 字符串要解析为 object,其中第一个设置了一些默认属性,第二个设置了一些,例如
请注意,属性 值可以是除 string
.
之外的任何其他类型
Json 字符串 1:
{
"Title": "Startpage",
"Link": "/index",
}
Json 字符串 2:
{
"Head": "Latest news"
"Link": "/news"
}
使用上面的 2 个 Json 字符串,我想要一个 object 结果:
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
如上例所示,如果第二个属性设置了 values/is,它将替换第一个中的值(与“Head”和“Link”一样),如果没有,则替换现有的价值持续存在(与“标题”一样)
我对这个新版本了解不多plug-in,但是我找到了一个教程可以跟着tutorial with some examples
基于他我想到了这个方法,我想他能解决他的问题
//To populate an existing variable we will do so, we will create a variable with the pre existing data
object PrevData = YourVariableData;
//After this we will map the json received
var NewObj = JsonSerializer.Parse<T>(jsonstring);
CopyValues(NewObj, PrevData)
//I found a function that does what you need, you can use it
//source:
public void CopyValues<T>(T target, T source)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (source== null) throw new ArgumentNullException(nameof(source));
Type t = typeof(T);
var properties = t.GetProperties(
BindingFlags.Instance | BindingFlags.Public).Where(prop =>
prop.CanRead
&& prop.CanWrite
&& prop.GetIndexParameters().Length == 0);
foreach (var prop in properties)
{
var value = prop.GetValue(source, null);
prop.SetValue(target, value, null);
}
}
如果您已经在您的项目中使用了 AutoMapper 或者不介意依赖它,您可以通过以下方式合并对象:
var configuration = new MapperConfiguration(cfg => cfg
.CreateMap<Model, Model>()
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != default)));
var mapper = configuration.CreateMapper();
var destination = new Model {Title = "Startpage", Link = "/index"};
var source = new Model {Head = "Latest news", Link = "/news"};
mapper.Map(source, destination);
class Model
{
public string Head { get; set; }
public string Title { get; set; }
public string Link { get; set; }
}
我不确定这是否能解决您的问题,但它应该可以作为临时解决方法。我所做的只是编写了一个简单的 class,其中包含一个 populateobject 方法。
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
我将其放入控制台应用程序以进行测试。如果您想自己测试,下面是整个应用程序。
using System;
using System.Text.Json;
using System.IO;
using System.Text.Json.Serialization;
namespace JsonQuestion1
{
class Program
{
static void Main(string[] args)
{
// Only used for testing
string path = @"C:\Users\Path\To\JsonFiles";
string st1 = File.ReadAllText(path + @"\st1.json");
string st2 = File.ReadAllText(path + @"\st2.json");
// Only used for testing ^^^
string myObject = MyDeserializer.PopulateObject(new[] { st1, st2 } );
Console.WriteLine(myObject);
Console.ReadLine();
}
}
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
}
Json 文件内容:
st1.json
{
"Title": "Startpage",
"Link": "/index"
}
st2.json
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
所以假设 Core 3 不支持开箱即用,让我们尝试解决这个问题。那么,我们的问题是什么?
我们想要一种方法,用 json 字符串中的属性覆盖现有 object 的某些属性。所以我们的方法将有一个签名:
void PopulateObject<T>(T target, string jsonSource) where T : class
我们真的不想要任何自定义解析,因为它很麻烦,所以我们将尝试显而易见的方法 - 反序列化 jsonSource
并将结果属性复制到我们的 object 中。然而,我们不能就这么走了
T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);
那是因为对于一个类型
class Example
{
int Id { get; set; }
int Value { get; set; }
}
和一个JSON
{
"Id": 42
}
我们会得到updateObject.Value == 0
。现在我们不知道 0
是新的更新值还是它只是没有更新,所以我们需要确切地知道 jsonSource
包含哪些属性。
幸运的是,System.Text.Json
API 允许我们检查已解析的 JSON.
的结构
using var json = JsonDocument.Parse(jsonSource).RootElement;
我们现在可以枚举所有属性并复制它们。
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
我们将使用反射复制值:
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
我们可以在这里看到我们正在做的是浅更新。如果 object 包含另一个复合体 object 作为它的 属性,那个复合体将作为一个整体被复制和覆盖,而不是更新。如果需要 deep 更新,则需要更改此方法以提取 属性 的当前值,然后如果 属性 递归调用 PopulateObject
] 的类型是引用类型(也需要接受 Type
作为 PopulateObject
中的参数)。
将所有这些加起来我们得到:
void PopulateObject<T>(T target, string jsonSource) where T : class
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
}
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
这有多稳健?好吧,它肯定不会对 JSON 数组做任何有意义的事情,但我不确定您如何期望 PopulateObject
方法开始对数组起作用。我不知道它与 Json.Net
版本的性能相比如何,您必须自己进行测试。根据设计,它还会默默地忽略不在目标类型中的属性。我认为这是最明智的方法,但您可能不这么认为,在那种情况下 属性 null-check 必须用异常抛出替换。
编辑:
我继续实施深拷贝:
void PopulateObject<T>(T target, string jsonSource) where T : class =>
PopulateObject(target, jsonSource, typeof(T));
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
OverwriteProperty(target, updatedProperty, typeof(T));
void PopulateObject(object target, string jsonSource, Type type)
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property, type);
}
}
void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
为了使这个更健壮,你要么必须有一个单独的 PopulateObjectDeep
方法,要么传递 PopulateObjectOptions
或带有 deep/shallow 标志的类似方法。
编辑 2:
deep-copying的重点是如果我们有一个object
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 32
},
"Value": 128
}
并用
填充它
{
"Child":
{
"Value": 64
}
}
我们会得到
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 64
},
"Value": 128
}
如果是浅拷贝,我们会在复制的 child.
中得到 Id = 0
编辑 3:
正如@ldam 指出的那样,这在稳定的 .NET Core 3.0 中不再有效,因为 API 已更改。 Parse
方法现在是 Deserialize
,您必须更深入地挖掘才能获得 JsonElement
的值。 an active issue in the corefx repo 允许直接反序列化 JsonElement
。现在最接近的解决方案是使用 GetRawText()
。我继续编辑上面的代码,留下旧版本 struck-through.
如果它只是一种用法并且你不想添加额外的依赖项/大量代码,你不介意有点低效率和我没有错过很明显,你可以使用:
private static T ParseWithTemplate<T>(T template, string input)
{
var ignoreNulls = new JsonSerializerOptions() { IgnoreNullValues = true };
var templateJson = JsonSerializer.ToString(template, ignoreNulls);
var combinedData = templateJson.TrimEnd('}') + "," + input.TrimStart().TrimStart('{');
return JsonSerializer.Parse<T>(combinedData);
}
下面是一些示例代码。它使用新的 Utf8JsonReader struct 因此它在解析对象的同时填充对象。它支持 JSON/CLR 类型等价、嵌套对象(如果不存在则创建)、列表和数组。
var populator = new JsonPopulator();
var obj = new MyClass();
populator.PopulateObject(obj, "{\"Title\":\"Startpage\",\"Link\":\"/index\"}");
populator.PopulateObject(obj, "{\"Head\":\"Latest news\",\"Link\":\"/news\"}");
public class MyClass
{
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
}
请注意,它不支持您可能期望的所有内容,但您可以覆盖或自定义它。可以添加的内容:1) 命名约定。您必须重写 GetProperty 方法。 2) 字典或 expando 对象。 3) 可以提高性能,因为它使用反射而不是MemberAccessor/delegate技术
public class JsonPopulator
{
public void PopulateObject(object obj, string jsonString, JsonSerializerOptions options = null) => PopulateObject(obj, jsonString != null ? Encoding.UTF8.GetBytes(jsonString) : null, options);
public virtual void PopulateObject(object obj, ReadOnlySpan<byte> jsonData, JsonSerializerOptions options = null)
{
options ??= new JsonSerializerOptions();
var state = new JsonReaderState(new JsonReaderOptions { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling, MaxDepth = options.MaxDepth });
var reader = new Utf8JsonReader(jsonData, isFinalBlock: true, state);
new Worker(this, reader, obj, options);
}
protected virtual PropertyInfo GetProperty(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = obj.GetType().GetProperty(propertyName);
return prop;
}
protected virtual bool SetPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = GetProperty(ref reader, options, obj, propertyName);
if (prop == null)
return false;
if (!TryReadPropertyValue(ref reader, options, prop.PropertyType, out var value))
return false;
prop.SetValue(obj, value);
return true;
}
protected virtual bool TryReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (reader.TokenType == JsonTokenType.Null)
{
value = null;
return !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null;
}
if (propertyType == typeof(object)) { value = ReadValue(ref reader); return true; }
if (propertyType == typeof(string)) { value = JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); return true; }
if (propertyType == typeof(int) && reader.TryGetInt32(out var i32)) { value = i32; return true; }
if (propertyType == typeof(long) && reader.TryGetInt64(out var i64)) { value = i64; return true; }
if (propertyType == typeof(DateTime) && reader.TryGetDateTime(out var dt)) { value = dt; return true; }
if (propertyType == typeof(DateTimeOffset) && reader.TryGetDateTimeOffset(out var dto)) { value = dto; return true; }
if (propertyType == typeof(Guid) && reader.TryGetGuid(out var guid)) { value = guid; return true; }
if (propertyType == typeof(decimal) && reader.TryGetDecimal(out var dec)) { value = dec; return true; }
if (propertyType == typeof(double) && reader.TryGetDouble(out var dbl)) { value = dbl; return true; }
if (propertyType == typeof(float) && reader.TryGetSingle(out var sgl)) { value = sgl; return true; }
if (propertyType == typeof(uint) && reader.TryGetUInt32(out var ui32)) { value = ui32; return true; }
if (propertyType == typeof(ulong) && reader.TryGetUInt64(out var ui64)) { value = ui64; return true; }
if (propertyType == typeof(byte[]) && reader.TryGetBytesFromBase64(out var bytes)) { value = bytes; return true; }
if (propertyType == typeof(bool))
{
if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True)
{
value = reader.GetBoolean();
return true;
}
}
// fallback here
return TryConvertValue(ref reader, propertyType, out value);
}
protected virtual object ReadValue(ref Utf8JsonReader reader)
{
switch (reader.TokenType)
{
case JsonTokenType.False: return false;
case JsonTokenType.True: return true;
case JsonTokenType.Null: return null;
case JsonTokenType.String: return reader.GetString();
case JsonTokenType.Number: // is there a better way?
if (reader.TryGetInt32(out var i32))
return i32;
if (reader.TryGetInt64(out var i64))
return i64;
if (reader.TryGetUInt64(out var ui64)) // uint is already handled by i64
return ui64;
if (reader.TryGetSingle(out var sgl))
return sgl;
if (reader.TryGetDouble(out var dbl))
return dbl;
if (reader.TryGetDecimal(out var dec))
return dec;
break;
}
throw new NotSupportedException();
}
// we're here when json types & property types don't match exactly
protected virtual bool TryConvertValue(ref Utf8JsonReader reader, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (propertyType == typeof(bool))
{
if (reader.TryGetInt64(out var i64)) // one size fits all
{
value = i64 != 0;
return true;
}
}
// TODO: add other conversions
value = null;
return false;
}
protected virtual object CreateInstance(ref Utf8JsonReader reader, Type propertyType)
{
if (propertyType.GetConstructor(Type.EmptyTypes) == null)
return null;
// TODO: handle custom instance creation
try
{
return Activator.CreateInstance(propertyType);
}
catch
{
// swallow
return null;
}
}
private class Worker
{
private readonly Stack<WorkerProperty> _properties = new Stack<WorkerProperty>();
private readonly Stack<object> _objects = new Stack<object>();
public Worker(JsonPopulator populator, Utf8JsonReader reader, object obj, JsonSerializerOptions options)
{
_objects.Push(obj);
WorkerProperty prop;
WorkerProperty peek;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
prop = new WorkerProperty();
prop.PropertyName = Encoding.UTF8.GetString(reader.ValueSpan);
_properties.Push(prop);
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
if (_properties.Count > 0)
{
object child = null;
var parent = _objects.Peek();
PropertyInfo pi = null;
if (parent != null)
{
pi = populator.GetProperty(ref reader, options, parent, _properties.Peek().PropertyName);
if (pi != null)
{
child = pi.GetValue(parent); // mimic ObjectCreationHandling.Auto
if (child == null && pi.CanWrite)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
if (!typeof(IList).IsAssignableFrom(pi.PropertyType))
break; // don't create if we can't handle it
}
if (reader.TokenType == JsonTokenType.StartArray && pi.PropertyType.IsArray)
{
child = Activator.CreateInstance(typeof(List<>).MakeGenericType(pi.PropertyType.GetElementType())); // we can't add to arrays...
}
else
{
child = populator.CreateInstance(ref reader, pi.PropertyType);
if (child != null)
{
pi.SetValue(parent, child);
}
}
}
}
}
if (reader.TokenType == JsonTokenType.StartObject)
{
_objects.Push(child);
}
else if (child != null) // StartArray
{
peek = _properties.Peek();
peek.IsArray = pi.PropertyType.IsArray;
peek.List = (IList)child;
peek.ListPropertyType = GetListElementType(child.GetType());
peek.ArrayPropertyInfo = pi;
}
}
break;
case JsonTokenType.EndObject:
_objects.Pop();
if (_properties.Count > 0)
{
_properties.Pop();
}
break;
case JsonTokenType.EndArray:
if (_properties.Count > 0)
{
prop = _properties.Pop();
if (prop.IsArray)
{
var array = Array.CreateInstance(GetListElementType(prop.ArrayPropertyInfo.PropertyType), prop.List.Count); // array is finished, convert list into a real array
prop.List.CopyTo(array, 0);
prop.ArrayPropertyInfo.SetValue(_objects.Peek(), array);
}
}
break;
case JsonTokenType.False:
case JsonTokenType.Null:
case JsonTokenType.Number:
case JsonTokenType.String:
case JsonTokenType.True:
peek = _properties.Peek();
if (peek.List != null)
{
if (populator.TryReadPropertyValue(ref reader, options, peek.ListPropertyType, out var item))
{
peek.List.Add(item);
}
break;
}
prop = _properties.Pop();
var current = _objects.Peek();
if (current != null)
{
populator.SetPropertyValue(ref reader, options, current, prop.PropertyName);
}
break;
}
}
}
private static Type GetListElementType(Type type)
{
if (type.IsArray)
return type.GetElementType();
foreach (Type iface in type.GetInterfaces())
{
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) return iface.GetGenericArguments()[1];
if (iface.GetGenericTypeDefinition() == typeof(IList<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(ICollection<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return iface.GetGenericArguments()[0];
}
return typeof(object);
}
}
private class WorkerProperty
{
public string PropertyName;
public IList List;
public Type ListPropertyType;
public bool IsArray;
public PropertyInfo ArrayPropertyInfo;
public override string ToString() => PropertyName;
}
}
解决方法也可以这么简单(也支持multi-level JSON):
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
namespace ConsoleApp
{
public class Model
{
public Model()
{
SubModel = new SubModel();
}
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
public SubModel SubModel { get; set; }
}
public class SubModel
{
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
{
var model = new Model();
Console.WriteLine(JsonSerializer.ToString(model));
var json1 = "{ \"Title\": \"Startpage\", \"Link\": \"/index\" }";
model = Map<Model>(model, json1);
Console.WriteLine(JsonSerializer.ToString(model));
var json2 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Name\": \"Reyan Chougle\" } }";
model = Map<Model>(model, json2);
Console.WriteLine(JsonSerializer.ToString(model));
var json3 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Engineer\" } }";
model = Map<Model>(model, json3);
Console.WriteLine(JsonSerializer.ToString(model));
var json4 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Programmer\" } }";
model = Map<Model>(model, json4);
Console.WriteLine(JsonSerializer.ToString(model));
Console.ReadKey();
}
public static T Map<T>(T obj, string jsonString) where T : class
{
var newObj = JsonSerializer.Parse<T>(jsonString);
foreach (var property in newObj.GetType().GetProperties())
{
if (obj.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(newObj) != null))
{
if (property.GetType().IsClass && property.PropertyType.Assembly.FullName == typeof(T).Assembly.FullName)
{
MethodInfo mapMethod = typeof(Program).GetMethod("Map");
MethodInfo genericMethod = mapMethod.MakeGenericMethod(property.GetValue(newObj).GetType());
var obj2 = genericMethod.Invoke(null, new object[] { property.GetValue(newObj), JsonSerializer.ToString(property.GetValue(newObj)) });
foreach (var property2 in obj2.GetType().GetProperties())
{
if (property2.GetValue(obj2) != null)
{
property.GetValue(obj).GetType().GetProperty(property2.Name).SetValue(property.GetValue(obj), property2.GetValue(obj2));
}
}
}
else
{
property.SetValue(obj, property.GetValue(newObj));
}
}
}
return obj;
}
}
}
输出:
此代码基于V0ldek给出的答案。
如果它们是在属性上定义的,它会添加 自定义转换器 的使用。
仅更新 public Setter 的属性。
/// <summary>
/// Utility class for System.Text.Json
/// </summary>
public static class JsonUtility
{
/// <summary>
/// Update an objet from JSON data
/// </summary>
/// <param name="type">Type of the object to update</param>
/// <param name="target">Object to update</param>
/// <param name="jsonSource">JSON Data</param>
/// <remarks>This code is based on the answer given by V0ldek on Whosebug</remarks>
/// <see cref="
public static void PopulateObject(Type type, object target, string jsonSource, JsonSerializerOptions options)
{
var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
OverwriteProperty(property);
void OverwriteProperty(JsonProperty updatedProperty)
{
var propertyInfo = type.GetProperty(updatedProperty.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
if (!(propertyInfo?.SetMethod?.IsPublic).GetValueOrDefault())
return;
if (propertyInfo.GetCustomAttribute<JsonIgnoreAttribute>() != null)
return;
// If the property has a Converter attribute, we use it
var converter = GetJsonConverter(propertyInfo);
if (converter != null)
{
var serializerOptions = new JsonSerializerOptions(options);
serializerOptions.Converters.Add(converter);
var parsedValue = JsonSerializer.Deserialize(updatedProperty.Value.GetRawText(), propertyInfo.PropertyType, serializerOptions);
propertyInfo.SetValue(target, parsedValue);
}
else
{
var parsedValue = JsonSerializer.Deserialize(updatedProperty.Value.GetRawText(), propertyInfo.PropertyType, options);
propertyInfo.SetValue(target, parsedValue);
}
}
}
/// <summary>
/// Return the JSON Converter of a property (null if not exists)
/// </summary>
/// <param name="propertyInfo">Property</param>
/// <see cref="https://github.com/dotnet/runtime/blob/v6.0.3/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs"/>
public static JsonConverter GetJsonConverter(PropertyInfo propertyInfo)
{
var attribute = propertyInfo.GetCustomAttribute<JsonConverterAttribute>();
if (attribute != null)
{
if (attribute.ConverterType == null)
return attribute.CreateConverter(propertyInfo.PropertyType);
else
{
var ctor = attribute.ConverterType.GetConstructor(Type.EmptyTypes);
if (typeof(JsonConverter).IsAssignableFrom(attribute.ConverterType) && (ctor?.IsPublic).GetValueOrDefault())
return (JsonConverter)Activator.CreateInstance(attribute.ConverterType)!;
}
}
return null;
}
}
我正在准备从 ASP.NET Core 2.2 迁移到 3.0。
因为我不使用更高级的 JSON 功能(但可能如下所述),并且 3.0 现在为 JSON 提供了 built-in namespace/classes ,System.Text.Json
,我决定看看能不能去掉之前的默认Newtonsoft.Json
.
请注意,我知道 System.Text.Json
不会完全取代 Newtonsoft.Json
。
我到处都做到了,例如
var obj = JsonSerializer.Parse<T>(jsonstring);
var jsonstring = JsonSerializer.ToString(obj);
但在一个地方,我填充了现有的 object。
有了Newtonsoft.Json
一个人就可以做到
JsonConvert.PopulateObject(jsonstring, obj);
built-in System.Text.Json
命名空间有一些额外的 类,比如 JsonDocumnet
、JsonElement
和 Utf8JsonReader
,虽然我不能找到任何将现有 object 作为参数的。
我也没有足够的经验来了解如何利用现有的。
link 可能有 a possible upcoming feature in .Net Core (thanks to Mustafa Gursel),但与此同时(如果没有怎么办)...
...我现在想知道,是否有可能实现与 PopulateObject
类似的东西?
我的意思是,是否有可能与其他 System.Text.Json
类 中的任何一个一起完成相同的任务,而 update/replace 仅设置属性?,...或其他一些聪明的解决方法?
这是我正在寻找的示例 input/output,它需要是通用的,因为传递给反序列化方法的 object 是 <T>
类型)。我有 2 个 Json 字符串要解析为 object,其中第一个设置了一些默认属性,第二个设置了一些,例如
请注意,属性 值可以是除 string
.
Json 字符串 1:
{
"Title": "Startpage",
"Link": "/index",
}
Json 字符串 2:
{
"Head": "Latest news"
"Link": "/news"
}
使用上面的 2 个 Json 字符串,我想要一个 object 结果:
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
如上例所示,如果第二个属性设置了 values/is,它将替换第一个中的值(与“Head”和“Link”一样),如果没有,则替换现有的价值持续存在(与“标题”一样)
我对这个新版本了解不多plug-in,但是我找到了一个教程可以跟着tutorial with some examples
基于他我想到了这个方法,我想他能解决他的问题
//To populate an existing variable we will do so, we will create a variable with the pre existing data
object PrevData = YourVariableData;
//After this we will map the json received
var NewObj = JsonSerializer.Parse<T>(jsonstring);
CopyValues(NewObj, PrevData)
//I found a function that does what you need, you can use it
//source:
public void CopyValues<T>(T target, T source)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (source== null) throw new ArgumentNullException(nameof(source));
Type t = typeof(T);
var properties = t.GetProperties(
BindingFlags.Instance | BindingFlags.Public).Where(prop =>
prop.CanRead
&& prop.CanWrite
&& prop.GetIndexParameters().Length == 0);
foreach (var prop in properties)
{
var value = prop.GetValue(source, null);
prop.SetValue(target, value, null);
}
}
如果您已经在您的项目中使用了 AutoMapper 或者不介意依赖它,您可以通过以下方式合并对象:
var configuration = new MapperConfiguration(cfg => cfg
.CreateMap<Model, Model>()
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != default)));
var mapper = configuration.CreateMapper();
var destination = new Model {Title = "Startpage", Link = "/index"};
var source = new Model {Head = "Latest news", Link = "/news"};
mapper.Map(source, destination);
class Model
{
public string Head { get; set; }
public string Title { get; set; }
public string Link { get; set; }
}
我不确定这是否能解决您的问题,但它应该可以作为临时解决方法。我所做的只是编写了一个简单的 class,其中包含一个 populateobject 方法。
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
我将其放入控制台应用程序以进行测试。如果您想自己测试,下面是整个应用程序。
using System;
using System.Text.Json;
using System.IO;
using System.Text.Json.Serialization;
namespace JsonQuestion1
{
class Program
{
static void Main(string[] args)
{
// Only used for testing
string path = @"C:\Users\Path\To\JsonFiles";
string st1 = File.ReadAllText(path + @"\st1.json");
string st2 = File.ReadAllText(path + @"\st2.json");
// Only used for testing ^^^
string myObject = MyDeserializer.PopulateObject(new[] { st1, st2 } );
Console.WriteLine(myObject);
Console.ReadLine();
}
}
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
}
Json 文件内容:
st1.json
{
"Title": "Startpage",
"Link": "/index"
}
st2.json
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
所以假设 Core 3 不支持开箱即用,让我们尝试解决这个问题。那么,我们的问题是什么?
我们想要一种方法,用 json 字符串中的属性覆盖现有 object 的某些属性。所以我们的方法将有一个签名:
void PopulateObject<T>(T target, string jsonSource) where T : class
我们真的不想要任何自定义解析,因为它很麻烦,所以我们将尝试显而易见的方法 - 反序列化 jsonSource
并将结果属性复制到我们的 object 中。然而,我们不能就这么走了
T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);
那是因为对于一个类型
class Example
{
int Id { get; set; }
int Value { get; set; }
}
和一个JSON
{
"Id": 42
}
我们会得到updateObject.Value == 0
。现在我们不知道 0
是新的更新值还是它只是没有更新,所以我们需要确切地知道 jsonSource
包含哪些属性。
幸运的是,System.Text.Json
API 允许我们检查已解析的 JSON.
using var json = JsonDocument.Parse(jsonSource).RootElement;
我们现在可以枚举所有属性并复制它们。
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
我们将使用反射复制值:
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
我们可以在这里看到我们正在做的是浅更新。如果 object 包含另一个复合体 object 作为它的 属性,那个复合体将作为一个整体被复制和覆盖,而不是更新。如果需要 deep 更新,则需要更改此方法以提取 属性 的当前值,然后如果 属性 递归调用 PopulateObject
] 的类型是引用类型(也需要接受 Type
作为 PopulateObject
中的参数)。
将所有这些加起来我们得到:
void PopulateObject<T>(T target, string jsonSource) where T : class
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
}
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
这有多稳健?好吧,它肯定不会对 JSON 数组做任何有意义的事情,但我不确定您如何期望 PopulateObject
方法开始对数组起作用。我不知道它与 Json.Net
版本的性能相比如何,您必须自己进行测试。根据设计,它还会默默地忽略不在目标类型中的属性。我认为这是最明智的方法,但您可能不这么认为,在那种情况下 属性 null-check 必须用异常抛出替换。
编辑:
我继续实施深拷贝:
void PopulateObject<T>(T target, string jsonSource) where T : class =>
PopulateObject(target, jsonSource, typeof(T));
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
OverwriteProperty(target, updatedProperty, typeof(T));
void PopulateObject(object target, string jsonSource, Type type)
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property, type);
}
}
void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
为了使这个更健壮,你要么必须有一个单独的 PopulateObjectDeep
方法,要么传递 PopulateObjectOptions
或带有 deep/shallow 标志的类似方法。
编辑 2:
deep-copying的重点是如果我们有一个object
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 32
},
"Value": 128
}
并用
填充它{
"Child":
{
"Value": 64
}
}
我们会得到
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 64
},
"Value": 128
}
如果是浅拷贝,我们会在复制的 child.
中得到Id = 0
编辑 3:
正如@ldam 指出的那样,这在稳定的 .NET Core 3.0 中不再有效,因为 API 已更改。 Parse
方法现在是 Deserialize
,您必须更深入地挖掘才能获得 JsonElement
的值。 an active issue in the corefx repo 允许直接反序列化 JsonElement
。现在最接近的解决方案是使用 GetRawText()
。我继续编辑上面的代码,留下旧版本 struck-through.
如果它只是一种用法并且你不想添加额外的依赖项/大量代码,你不介意有点低效率和我没有错过很明显,你可以使用:
private static T ParseWithTemplate<T>(T template, string input)
{
var ignoreNulls = new JsonSerializerOptions() { IgnoreNullValues = true };
var templateJson = JsonSerializer.ToString(template, ignoreNulls);
var combinedData = templateJson.TrimEnd('}') + "," + input.TrimStart().TrimStart('{');
return JsonSerializer.Parse<T>(combinedData);
}
下面是一些示例代码。它使用新的 Utf8JsonReader struct 因此它在解析对象的同时填充对象。它支持 JSON/CLR 类型等价、嵌套对象(如果不存在则创建)、列表和数组。
var populator = new JsonPopulator();
var obj = new MyClass();
populator.PopulateObject(obj, "{\"Title\":\"Startpage\",\"Link\":\"/index\"}");
populator.PopulateObject(obj, "{\"Head\":\"Latest news\",\"Link\":\"/news\"}");
public class MyClass
{
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
}
请注意,它不支持您可能期望的所有内容,但您可以覆盖或自定义它。可以添加的内容:1) 命名约定。您必须重写 GetProperty 方法。 2) 字典或 expando 对象。 3) 可以提高性能,因为它使用反射而不是MemberAccessor/delegate技术
public class JsonPopulator
{
public void PopulateObject(object obj, string jsonString, JsonSerializerOptions options = null) => PopulateObject(obj, jsonString != null ? Encoding.UTF8.GetBytes(jsonString) : null, options);
public virtual void PopulateObject(object obj, ReadOnlySpan<byte> jsonData, JsonSerializerOptions options = null)
{
options ??= new JsonSerializerOptions();
var state = new JsonReaderState(new JsonReaderOptions { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling, MaxDepth = options.MaxDepth });
var reader = new Utf8JsonReader(jsonData, isFinalBlock: true, state);
new Worker(this, reader, obj, options);
}
protected virtual PropertyInfo GetProperty(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = obj.GetType().GetProperty(propertyName);
return prop;
}
protected virtual bool SetPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = GetProperty(ref reader, options, obj, propertyName);
if (prop == null)
return false;
if (!TryReadPropertyValue(ref reader, options, prop.PropertyType, out var value))
return false;
prop.SetValue(obj, value);
return true;
}
protected virtual bool TryReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (reader.TokenType == JsonTokenType.Null)
{
value = null;
return !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null;
}
if (propertyType == typeof(object)) { value = ReadValue(ref reader); return true; }
if (propertyType == typeof(string)) { value = JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); return true; }
if (propertyType == typeof(int) && reader.TryGetInt32(out var i32)) { value = i32; return true; }
if (propertyType == typeof(long) && reader.TryGetInt64(out var i64)) { value = i64; return true; }
if (propertyType == typeof(DateTime) && reader.TryGetDateTime(out var dt)) { value = dt; return true; }
if (propertyType == typeof(DateTimeOffset) && reader.TryGetDateTimeOffset(out var dto)) { value = dto; return true; }
if (propertyType == typeof(Guid) && reader.TryGetGuid(out var guid)) { value = guid; return true; }
if (propertyType == typeof(decimal) && reader.TryGetDecimal(out var dec)) { value = dec; return true; }
if (propertyType == typeof(double) && reader.TryGetDouble(out var dbl)) { value = dbl; return true; }
if (propertyType == typeof(float) && reader.TryGetSingle(out var sgl)) { value = sgl; return true; }
if (propertyType == typeof(uint) && reader.TryGetUInt32(out var ui32)) { value = ui32; return true; }
if (propertyType == typeof(ulong) && reader.TryGetUInt64(out var ui64)) { value = ui64; return true; }
if (propertyType == typeof(byte[]) && reader.TryGetBytesFromBase64(out var bytes)) { value = bytes; return true; }
if (propertyType == typeof(bool))
{
if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True)
{
value = reader.GetBoolean();
return true;
}
}
// fallback here
return TryConvertValue(ref reader, propertyType, out value);
}
protected virtual object ReadValue(ref Utf8JsonReader reader)
{
switch (reader.TokenType)
{
case JsonTokenType.False: return false;
case JsonTokenType.True: return true;
case JsonTokenType.Null: return null;
case JsonTokenType.String: return reader.GetString();
case JsonTokenType.Number: // is there a better way?
if (reader.TryGetInt32(out var i32))
return i32;
if (reader.TryGetInt64(out var i64))
return i64;
if (reader.TryGetUInt64(out var ui64)) // uint is already handled by i64
return ui64;
if (reader.TryGetSingle(out var sgl))
return sgl;
if (reader.TryGetDouble(out var dbl))
return dbl;
if (reader.TryGetDecimal(out var dec))
return dec;
break;
}
throw new NotSupportedException();
}
// we're here when json types & property types don't match exactly
protected virtual bool TryConvertValue(ref Utf8JsonReader reader, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (propertyType == typeof(bool))
{
if (reader.TryGetInt64(out var i64)) // one size fits all
{
value = i64 != 0;
return true;
}
}
// TODO: add other conversions
value = null;
return false;
}
protected virtual object CreateInstance(ref Utf8JsonReader reader, Type propertyType)
{
if (propertyType.GetConstructor(Type.EmptyTypes) == null)
return null;
// TODO: handle custom instance creation
try
{
return Activator.CreateInstance(propertyType);
}
catch
{
// swallow
return null;
}
}
private class Worker
{
private readonly Stack<WorkerProperty> _properties = new Stack<WorkerProperty>();
private readonly Stack<object> _objects = new Stack<object>();
public Worker(JsonPopulator populator, Utf8JsonReader reader, object obj, JsonSerializerOptions options)
{
_objects.Push(obj);
WorkerProperty prop;
WorkerProperty peek;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
prop = new WorkerProperty();
prop.PropertyName = Encoding.UTF8.GetString(reader.ValueSpan);
_properties.Push(prop);
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
if (_properties.Count > 0)
{
object child = null;
var parent = _objects.Peek();
PropertyInfo pi = null;
if (parent != null)
{
pi = populator.GetProperty(ref reader, options, parent, _properties.Peek().PropertyName);
if (pi != null)
{
child = pi.GetValue(parent); // mimic ObjectCreationHandling.Auto
if (child == null && pi.CanWrite)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
if (!typeof(IList).IsAssignableFrom(pi.PropertyType))
break; // don't create if we can't handle it
}
if (reader.TokenType == JsonTokenType.StartArray && pi.PropertyType.IsArray)
{
child = Activator.CreateInstance(typeof(List<>).MakeGenericType(pi.PropertyType.GetElementType())); // we can't add to arrays...
}
else
{
child = populator.CreateInstance(ref reader, pi.PropertyType);
if (child != null)
{
pi.SetValue(parent, child);
}
}
}
}
}
if (reader.TokenType == JsonTokenType.StartObject)
{
_objects.Push(child);
}
else if (child != null) // StartArray
{
peek = _properties.Peek();
peek.IsArray = pi.PropertyType.IsArray;
peek.List = (IList)child;
peek.ListPropertyType = GetListElementType(child.GetType());
peek.ArrayPropertyInfo = pi;
}
}
break;
case JsonTokenType.EndObject:
_objects.Pop();
if (_properties.Count > 0)
{
_properties.Pop();
}
break;
case JsonTokenType.EndArray:
if (_properties.Count > 0)
{
prop = _properties.Pop();
if (prop.IsArray)
{
var array = Array.CreateInstance(GetListElementType(prop.ArrayPropertyInfo.PropertyType), prop.List.Count); // array is finished, convert list into a real array
prop.List.CopyTo(array, 0);
prop.ArrayPropertyInfo.SetValue(_objects.Peek(), array);
}
}
break;
case JsonTokenType.False:
case JsonTokenType.Null:
case JsonTokenType.Number:
case JsonTokenType.String:
case JsonTokenType.True:
peek = _properties.Peek();
if (peek.List != null)
{
if (populator.TryReadPropertyValue(ref reader, options, peek.ListPropertyType, out var item))
{
peek.List.Add(item);
}
break;
}
prop = _properties.Pop();
var current = _objects.Peek();
if (current != null)
{
populator.SetPropertyValue(ref reader, options, current, prop.PropertyName);
}
break;
}
}
}
private static Type GetListElementType(Type type)
{
if (type.IsArray)
return type.GetElementType();
foreach (Type iface in type.GetInterfaces())
{
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) return iface.GetGenericArguments()[1];
if (iface.GetGenericTypeDefinition() == typeof(IList<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(ICollection<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return iface.GetGenericArguments()[0];
}
return typeof(object);
}
}
private class WorkerProperty
{
public string PropertyName;
public IList List;
public Type ListPropertyType;
public bool IsArray;
public PropertyInfo ArrayPropertyInfo;
public override string ToString() => PropertyName;
}
}
解决方法也可以这么简单(也支持multi-level JSON):
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
namespace ConsoleApp
{
public class Model
{
public Model()
{
SubModel = new SubModel();
}
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
public SubModel SubModel { get; set; }
}
public class SubModel
{
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
{
var model = new Model();
Console.WriteLine(JsonSerializer.ToString(model));
var json1 = "{ \"Title\": \"Startpage\", \"Link\": \"/index\" }";
model = Map<Model>(model, json1);
Console.WriteLine(JsonSerializer.ToString(model));
var json2 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Name\": \"Reyan Chougle\" } }";
model = Map<Model>(model, json2);
Console.WriteLine(JsonSerializer.ToString(model));
var json3 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Engineer\" } }";
model = Map<Model>(model, json3);
Console.WriteLine(JsonSerializer.ToString(model));
var json4 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Programmer\" } }";
model = Map<Model>(model, json4);
Console.WriteLine(JsonSerializer.ToString(model));
Console.ReadKey();
}
public static T Map<T>(T obj, string jsonString) where T : class
{
var newObj = JsonSerializer.Parse<T>(jsonString);
foreach (var property in newObj.GetType().GetProperties())
{
if (obj.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(newObj) != null))
{
if (property.GetType().IsClass && property.PropertyType.Assembly.FullName == typeof(T).Assembly.FullName)
{
MethodInfo mapMethod = typeof(Program).GetMethod("Map");
MethodInfo genericMethod = mapMethod.MakeGenericMethod(property.GetValue(newObj).GetType());
var obj2 = genericMethod.Invoke(null, new object[] { property.GetValue(newObj), JsonSerializer.ToString(property.GetValue(newObj)) });
foreach (var property2 in obj2.GetType().GetProperties())
{
if (property2.GetValue(obj2) != null)
{
property.GetValue(obj).GetType().GetProperty(property2.Name).SetValue(property.GetValue(obj), property2.GetValue(obj2));
}
}
}
else
{
property.SetValue(obj, property.GetValue(newObj));
}
}
}
return obj;
}
}
}
输出:
此代码基于V0ldek给出的答案。 如果它们是在属性上定义的,它会添加 自定义转换器 的使用。 仅更新 public Setter 的属性。
/// <summary>
/// Utility class for System.Text.Json
/// </summary>
public static class JsonUtility
{
/// <summary>
/// Update an objet from JSON data
/// </summary>
/// <param name="type">Type of the object to update</param>
/// <param name="target">Object to update</param>
/// <param name="jsonSource">JSON Data</param>
/// <remarks>This code is based on the answer given by V0ldek on Whosebug</remarks>
/// <see cref="
public static void PopulateObject(Type type, object target, string jsonSource, JsonSerializerOptions options)
{
var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
OverwriteProperty(property);
void OverwriteProperty(JsonProperty updatedProperty)
{
var propertyInfo = type.GetProperty(updatedProperty.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
if (!(propertyInfo?.SetMethod?.IsPublic).GetValueOrDefault())
return;
if (propertyInfo.GetCustomAttribute<JsonIgnoreAttribute>() != null)
return;
// If the property has a Converter attribute, we use it
var converter = GetJsonConverter(propertyInfo);
if (converter != null)
{
var serializerOptions = new JsonSerializerOptions(options);
serializerOptions.Converters.Add(converter);
var parsedValue = JsonSerializer.Deserialize(updatedProperty.Value.GetRawText(), propertyInfo.PropertyType, serializerOptions);
propertyInfo.SetValue(target, parsedValue);
}
else
{
var parsedValue = JsonSerializer.Deserialize(updatedProperty.Value.GetRawText(), propertyInfo.PropertyType, options);
propertyInfo.SetValue(target, parsedValue);
}
}
}
/// <summary>
/// Return the JSON Converter of a property (null if not exists)
/// </summary>
/// <param name="propertyInfo">Property</param>
/// <see cref="https://github.com/dotnet/runtime/blob/v6.0.3/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs"/>
public static JsonConverter GetJsonConverter(PropertyInfo propertyInfo)
{
var attribute = propertyInfo.GetCustomAttribute<JsonConverterAttribute>();
if (attribute != null)
{
if (attribute.ConverterType == null)
return attribute.CreateConverter(propertyInfo.PropertyType);
else
{
var ctor = attribute.ConverterType.GetConstructor(Type.EmptyTypes);
if (typeof(JsonConverter).IsAssignableFrom(attribute.ConverterType) && (ctor?.IsPublic).GetValueOrDefault())
return (JsonConverter)Activator.CreateInstance(attribute.ConverterType)!;
}
}
return null;
}
}