.NET Core/System.Text.Json:枚举和添加/替换 json 属性/值

Posted

技术标签:

【中文标题】.NET Core/System.Text.Json:枚举和添加/替换 json 属性/值【英文标题】:.NET Core/System.Text.Json: Enumerate and add/replace json properties/values 【发布时间】:2022-01-08 09:20:02 【问题描述】:

在我之前的一个问题中,我问过如何使用 System.Text.Json populate an existing object。

One of the great answers 展示了使用JsonDocument 解析json 字符串并使用EnumerateObject 枚举它的解决方案。

随着时间的推移,我的 json 字符串不断发展,现在还包含一个对象数组,当使用链接答案中的代码对其进行解析时,它会引发以下异常:

The requested operation requires an element of type 'Object', but the target element has type 'Array'.

我发现一个人可以通过一种方式或另一种方式寻找JsonValueKind.Array,然后做这样的事情

if (json.ValueKind.Equals(JsonValueKind.Array))

    foreach (var item in json.EnumerateArray())
    
        foreach (var property in item.EnumerateObject())
        
            await OverwriteProperty(???);
        
    

但我做不到。

如何做到这一点,并作为通用解决方案?

我想获得 "Result 1",其中数组项被添加/更新,以及 "Result 2"(当传递一个变量时),其中整个数组被替换。

对于 “结果 2” 我假设可以在 OverwriteProperty 方法中检测到 if (JsonValueKind.Array)),以及在哪里/如何传递“replaceArray”变量? ...在迭代数组或对象时?

一些样本数据:

Json 字符串初始


  "Title": "Startpage",
  "Links": [
    
      "Id": 10,
      "Text": "Start",
      "Link": "/index"
    ,
    
      "Id": 11,
      "Text": "Info",
      "Link": "/info"
    
  ]

要添加/更新的 JSON 字符串


  "Head": "Latest news",
  "Links": [
    
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    ,
    
      "Id": 21,
      "Text": "More News",
      "Link": "/morenews"
    
  ]

结果 1


  "Title": "Startpage",
  "Head": "Latest news"
  "Links": [
    
      "Id": 10,
      "Text": "Start",
      "Link": "/indexnews"
    ,
    
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    ,
    
      "Id": 21,
      "Text": "More news",
      "Link": "/morenews"
    
  ]

结果 2


  "Title": "Startpage",
  "Head": "Latest news"
  "Links": [
    
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    ,
    
      "Id": 21,
      "Text": "More News",
      "Link": "/morenews"
    
  ]

public class Pages

    public string Title  get; set; 
    public string Head  get; set; 
    public List<Links> Links  get; set; 


public class Links

    public int Id  get; set; 
    public string Text  get; set; 
    public string Link  get; set; 

C#代码:

public async Task PopulateObjectAsync(object target, string source, Type type, bool replaceArrays = false)

    using var json = JsonDocument.Parse(source).RootElement;

    if (json.ValueKind.Equals(JsonValueKind.Array))
    
        foreach (var item in json.EnumerateArray())
        
            foreach (var property in item.EnumerateObject())
            
                await OverwriteProperty(???, replaceArray);  //use "replaceArray" here ?
            
        
    
    else
    
        foreach (var property in json.EnumerateObject())
        
            await OverwriteProperty(target, property, type, replaceArray);  //use "replaceArray" here ?
        
    

    return;


public async Task OverwriteProperty(object target, JsonProperty updatedProperty, Type type, bool replaceArrays)

    var propertyInfo = type.GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    
        return;
    

    var propertyType = propertyInfo.PropertyType;
    object parsedValue;

    if (propertyType.IsValueType)
    
        parsedValue = JsonSerializer.Deserialize(
            updatedProperty.Value.GetRawText(),
            propertyType);
    
    else if (replaceArrays && "property is JsonValueKind.Array")  //pseudo code sample
    
        // use same code here as in above "IsValueType" ?
    
    else
    
        parsedValue = propertyInfo.GetValue(target);

        await PopulateObjectAsync(
            parsedValue,
            updatedProperty.Value.GetRawText(),
            propertyType);
    

    propertyInfo.SetValue(target, parsedValue);

【问题讨论】:

这看起来像是 .NET Core 在.NET Core Configuration 上为他们的appsettings.json 文件做的事情。也许检查他们的源代码可能会有所帮助? @LukeVo -- 是的,尽管我需要的是链接答案的作用。我只需要小更新来处理数组。 MS 自己建议 custom converters 或使用 Utf8JsonReader struct,在链接的问题中也有答案。虽然它需要更多的工作。 您是在寻找适用于这种特定情况的解决方案(即您知道架构大多保留),还是寻找适用于所有类型对象的通用解决方案? @PeterCsala -- 我正在寻找通用解决方案。 @Daniel -- 好吧,我所拥有的 List&lt;Links&gt; 并不是不必要的,即使是这样,仍然需要能够解析包含可枚举 [ ... ] 的 JSON 字符串财产。 【参考方案1】:

好吧,如果您不关心数组的编写方式,我有一个简单的解决方案。在 2 个阶段中创建一个新 JSON,1 个循环用于新属性,1 个循环用于更新:

    var sourceJson = @"

  ""Title"": ""Startpage"",
  ""Links"": [
    
      ""Id"": 10,
      ""Text"": ""Start"",
      ""Link"": ""/index""
    ,
    
      ""Id"": 11,
      ""Text"": ""Info"",
      ""Link"": ""/info""
    
  ]
";
        var updateJson = @"

  ""Head"": ""Latest news"",
  ""Links"": [
    
      ""Id"": 11,
      ""Text"": ""News"",
      ""Link"": ""/news""
    ,
    
      ""Id"": 21,
      ""Text"": ""More News"",
      ""Link"": ""/morenews""
    
  ]

";
        using var source = JsonDocument.Parse(sourceJson);
        using var update = JsonDocument.Parse(updateJson);
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream);
        writer.WriteStartObject();
        // write non existing properties
        foreach (var prop in update.RootElement.EnumerateObject().Where(prop => !source.RootElement.TryGetProperty(prop.Name, out _)))
        
            prop.WriteTo(writer);
        

        // make updates for existing
        foreach (var prop in source.RootElement.EnumerateObject())
        
            if (update.RootElement.TryGetProperty(prop.Name, out var overwrite))
            
                writer.WritePropertyName(prop.Name);
                overwrite.WriteTo(writer);
            
            else
            
                prop.WriteTo(writer);
            
        

        writer.WriteEndObject();
        writer.Flush();
        var resultJson = Encoding.UTF8.GetString(stream.ToArray());
        Console.WriteLine(resultJson);

输出:


   "Head":"Latest news",
   "Title":"Startpage",
   "Links":[
      
         "Id":11,
         "Text":"News",
         "Link":"/news"
      ,
      
         "Id":21,
         "Text":"More News",
         "Link":"/morenews"
      
   ]

Fiddle

【讨论】:

谢谢...有趣的解决方案。将研究它,看看我如何利用它。不过,我喜欢使用 System.Text.Json 的解决方案,所以我会给你最初的赏金。【参考方案2】:

预赛

我将大量使用我对链接问题的回答中的现有代码:.Net Core 3.0 JsonSerializer populate existing object。

正如我提到的,浅拷贝的代码可以工作并产生结果 2。所以我们只需要修复深拷贝的代码并让它产生结果 1。

在我的机器上,当propertyTypetypeof(string) 时,代码在PopulateObject 中崩溃,因为string 既不是值类型也不是JSON 中的对象所表示的东西。我在原始答案中修复了这个问题,if 必须是:

if (elementType.IsValueType || elementType == typeof(string))

实施新要求

好的,所以第一个问题是识别某个东西是否是一个集合。目前我们查看我们想要覆盖的属性的类型来做出决定,所以现在我们将做同样的事情。逻辑如下:

private static bool IsCollection(Type type) =>
        type.GetInterfaces().Any(x => x.IsGenericType && 
        x.GetGenericTypeDefinition() == typeof(ICollection<>));

所以我们认为集合的唯一事物是为某些 T 实现 ICollection&lt;T&gt; 的事物。我们将通过实现一个新的PopulateCollection 方法来完全独立地处理集合。我们还需要一种方法来构造一个新集合——也许初始对象中的列表是null,所以我们需要在填充它之前创建一个新集合。为此,我们将寻找它的无参数构造函数:

private static object Instantiate(Type type)

    var ctor =  type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Array.Empty<Type>());

    if (ctor is null)
    
        throw new InvalidOperationException($"Type type.Name has no parameterless constructor.");
    

    return ctor.Invoke(Array.Empty<object?>());

我们允许它是private,因为为什么不呢。

现在我们对OverwriteProperty进行一些更改:

    private static void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
    
        var propertyInfo = type.GetProperty(updatedProperty.Name);

        if (propertyInfo == null)
        
            return;
        

        if (updatedProperty.Value.ValueKind == JsonValueKind.Null)
        
            propertyInfo.SetValue(target, null);
            return;
        

        var propertyType = propertyInfo.PropertyType;
        object? parsedValue;

        if (propertyType.IsValueType || propertyType == typeof(string))
        
            parsedValue = JsonSerializer.Deserialize(
                updatedProperty.Value.GetRawText(),
                propertyType);
        
        else if (IsCollection(propertyType))
        
            var elementType = propertyType.GenericTypeArguments[0];
            parsedValue = propertyInfo.GetValue(target);
            parsedValue ??= Instantiate(propertyType);

            PopulateCollection(parsedValue, updatedProperty.Value.GetRawText(), elementType);
        
        else
        
            parsedValue = propertyInfo.GetValue(target);
            parsedValue ??= Instantiate(propertyType);

            PopulateObject(
                parsedValue,
                updatedProperty.Value.GetRawText(),
                propertyType);
        

        propertyInfo.SetValue(target, parsedValue);
    

最大的变化是if 语句的第二个分支。我们找出集合中元素的类型并从对象中提取现有集合。如果它为空,我们创建一个新的空的。然后我们调用新方法来填充它。

PopulateCollection 方法与OverwriteProperty 非常相似。

private static void PopulateCollection(object target, string jsonSource, Type elementType)

首先我们得到集合的Add方法:

var addMethod = target.GetType().GetMethod("Add", new[]  elementType );

这里我们期望一个实际的 JSON 数组,所以是时候枚举它了。对于数组中的每个元素,我们需要执行与OverwriteProperty 相同的操作,具体取决于我们是否有一个值、数组或对象,我们有不同的流程。

foreach (var property in json.EnumerateArray())

    object? element;

    if (elementType.IsValueType || elementType == typeof(string))
    
        element = JsonSerializer.Deserialize(jsonSource, elementType);
    
    else if (IsCollection(elementType))
    
        var nestedElementType = elementType.GenericTypeArguments[0];
        element = Instantiate(elementType);

        PopulateCollection(element, property.GetRawText(), nestedElementType);
    
    else
    
        element = Instantiate(elementType);

        PopulateObject(element, property.GetRawText(), elementType);
    

    addMethod.Invoke(target, new[]  element );

独特性

现在我们有一个问题。当前实现将始终添加到集合中,而不管其当前内容如何。所以这将返回的东西既不是结果 1 也不是结果 2,而是结果 3:


  "Title": "Startpage",
  "Head": "Latest news"
  "Links": [
    
      "Id": 10,
      "Text": "Start",
      "Link": "/indexnews"
    ,
    
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    ,
    
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    ,
    
      "Id": 21,
      "Text": "More news",
      "Link": "/morenews"
    
  ]

我们有一个包含链接 10 和 11 的数组,然后添加了另一个包含链接 11 和 12 的数组。没有明显的自然方式来处理这个问题。我在这里选择的设计决策是:集合决定元素是否已经存在。我们将在集合上调用默认的Contains 方法并添加当且仅当它返回false。它要求我们重写Links 上的Equals 方法来比较Id

public override bool Equals(object? obj) =>
    obj is Links other && Id == other.Id;

public override int GetHashCode() => Id.GetHashCode();

现在需要的更改是:

首先,获取Contains方法:
var containsMethod = target.GetType().GetMethod("Contains", new[]  elementType );
那么,在我们收到element 后检查一下:
var contains = containsMethod.Invoke(target, new[]  element );
if (contains is false)

    addMethod.Invoke(target, new[]  element );

测试

我在PagesLinks 类中添加了一些内容,首先我覆盖了ToString,以便我们可以轻松地检查我们的结果。然后,如前所述,我将Equals 覆盖为Links

public class Pages

    public string Title  get; set; 
    public string Head  get; set; 
    public List<Links> Links  get; set; 

    public override string ToString() => 
        $"Pages  Title = Title, Head = Head, Links = string.Join(", ", Links) ";


public class Links

    public int Id  get; set; 
    public string Text  get; set; 
    public string Link  get; set; 

    public override bool Equals(object? obj) =>
        obj is Links other && Id == other.Id;

    public override int GetHashCode() => Id.GetHashCode();

    public override string ToString() => $"Links  Id = Id, Text = Text, Link = Link ";

还有测试:

var initial = @"
  ""Title"": ""Startpage"",
  ""Links"": [
    
      ""Id"": 10,
      ""Text"": ""Start"",
      ""Link"": ""/index""
    ,
    
    ""Id"": 11,
      ""Text"": ""Info"",
      ""Link"": ""/info""
    
  ]
";

var update = @"
  ""Head"": ""Latest news"",
  ""Links"": [
    
      ""Id"": 11,
      ""Text"": ""News"",
      ""Link"": ""/news""
    ,
    
    ""Id"": 21,
      ""Text"": ""More News"",
      ""Link"": ""/morenews""
    
  ]
";

var pages = new Pages();

PopulateObject(pages, initial);

Console.WriteLine(pages);

PopulateObject(pages, update);

Console.WriteLine(pages);

结果:

Initial:
Pages  Title = Startpage, Head = , Links = Links  Id = 10, Text = Start, Link = /index , Links  Id = 11, Text = Info, Link = /info  
Update:
Pages  Title = Startpage, Head = Latest news, Links = Links  Id = 10, Text = Start, Link = /index , Links  Id = 11, Text = Info, Link = /info , Links  Id = 21, Text = More News, Link = /morenews  

你可以在this fiddle找到它。

限制

    我们使用Add 方法,因此这不适用于.NET 数组的属性,因为您不能Add 对它们。它们必须单独处理,首先创建元素,然后构造一个适当大小的数组并填充它。 使用Contains 的决定对我来说有点不确定。最好能更好地控制添加到集合中的内容。但这很简单且有效,因此对于 SO 答案来说就足够了。

最终代码

static class JsonUtils

    public static void PopulateObject<T>(T target, string jsonSource) where T : class =>
        PopulateObject(target, jsonSource, typeof(T));

    public static void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
        OverwriteProperty(target, updatedProperty, typeof(T));

    private static 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);
        
    

    private static void PopulateCollection(object target, string jsonSource, Type elementType)
    
        using var json = JsonDocument.Parse(jsonSource).RootElement;
        var addMethod = target.GetType().GetMethod("Add", new[]  elementType );
        var containsMethod = target.GetType().GetMethod("Contains", new[]  elementType );

        Debug.Assert(addMethod is not null);
        Debug.Assert(containsMethod is not null);

        foreach (var property in json.EnumerateArray())
        
            object? element;

            if (elementType.IsValueType || elementType == typeof(string))
            
                element = JsonSerializer.Deserialize(jsonSource, elementType);
            
            else if (IsCollection(elementType))
            
                var nestedElementType = elementType.GenericTypeArguments[0];
                element = Instantiate(elementType);

                PopulateCollection(element, property.GetRawText(), nestedElementType);
            
            else
            
                element = Instantiate(elementType);

                PopulateObject(element, property.GetRawText(), elementType);
            

            var contains = containsMethod.Invoke(target, new[]  element );
            if (contains is false)
            
                addMethod.Invoke(target, new[]  element );
            
        
    

    private static void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
    
        var propertyInfo = type.GetProperty(updatedProperty.Name);

        if (propertyInfo == null)
        
            return;
        

        if (updatedProperty.Value.ValueKind == JsonValueKind.Null)
        
            propertyInfo.SetValue(target, null);
            return;
        

        var propertyType = propertyInfo.PropertyType;
        object? parsedValue;

        if (propertyType.IsValueType || propertyType == typeof(string))
        
            parsedValue = JsonSerializer.Deserialize(
                updatedProperty.Value.GetRawText(),
                propertyType);
        
        else if (IsCollection(propertyType))
        
            var elementType = propertyType.GenericTypeArguments[0];
            parsedValue = propertyInfo.GetValue(target);
            parsedValue ??= Instantiate(propertyType);

            PopulateCollection(parsedValue, updatedProperty.Value.GetRawText(), elementType);
        
        else
        
            parsedValue = propertyInfo.GetValue(target);
            parsedValue ??= Instantiate(propertyType);

            PopulateObject(
                parsedValue,
                updatedProperty.Value.GetRawText(),
                propertyType);
        

        propertyInfo.SetValue(target, parsedValue);
    

    private static object Instantiate(Type type)
    
        var ctor =  type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Array.Empty<Type>());

        if (ctor is null)
        
            throw new InvalidOperationException($"Type type.Name has no parameterless constructor.");
        

        return ctor.Invoke(Array.Empty<object?>());
    

    private static bool IsCollection(Type type) =>
        type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>));

【讨论】:

谢谢...会在几天内研究这个,虽然它看起来很棒而且我已经开始了另一个赏金:)【参考方案3】:

这是在您想使用纯 JSON 解决方案的情况下,尽管我认为它并不比反射解决方案好多少。它绝对涵盖的用例比默认的JsonSerializer 少,例如您可能对IReadOnlyCollections 有问题。

public class JsonPopulator

    public static void PopulateObject(object target, string json, bool replaceArray)
    
        using var jsonDoc = JsonDocument.Parse(json);
        var root = jsonDoc.RootElement;

        // Simplify the process by making sure the first one is Object
        if (root.ValueKind != JsonValueKind.Object)
        
            throw new InvalidDataException("JSON Root must be a JSON Object");
        

        var type = target.GetType();
        foreach (var jsonProp in root.EnumerateObject())
        
            var prop = type.GetProperty(jsonProp.Name);

            if (prop == null || !prop.CanWrite)  continue; 

            var currValue = prop.GetValue(target);
            var value = ParseJsonValue(jsonProp.Value, prop.PropertyType, replaceArray, currValue);

            if (value != null)
            
                prop.SetValue(target, value);
            
        
    

    static object? ParseJsonValue(JsonElement value, Type type, bool replaceArray, object? initialValue)
    
        if (type.IsArray || type.IsAssignableTo(typeof(IEnumerable<object>)))
        
            // Array or List
            var initalArr = initialValue as IEnumerable<object>;

            // Get the type of the Array/List element
            var elType = GetElementType(type);

            var parsingValues = new List<object?>();
            foreach (var item in value.EnumerateArray())
            
                parsingValues.Add(ParseJsonValue(item, elType, replaceArray, null));
            

            List<object?> finalItems;
            if (replaceArray || initalArr == null)
            
                finalItems = parsingValues;
            
            else
            
                finalItems = initalArr.Concat(parsingValues).ToList();
            

            // Cast them to the correct type
            return CastIEnumrable(finalItems, type, elType);
        
        else if (type.IsValueType || type == typeof(string))
        
            // I don't think this is optimal but I will just use your code
            // since I assume it is working for you
            return JsonSerializer.Deserialize(
                value.GetRawText(),
                type);
        
        else
        
            // Assume it's object
            // Assuming it's object
            if (value.ValueKind != JsonValueKind.Object)
            
                throw new InvalidDataException("Expecting a JSON object");
            

            var finalValue = initialValue;

            // If it's null, the original object didn't have it yet
            // Initialize it using default constructor
            // You may need to check for JsonConstructor as well
            if (initialValue == null)
            
                var constructor = type.GetConstructor(Array.Empty<Type>());
                if (constructor == null)
                
                    throw new TypeAccessException($"type.Name does not have a default constructor.");
                

                finalValue = constructor.Invoke(Array.Empty<object>());
            

            foreach (var jsonProp in value.EnumerateObject())
            
                var subProp = type.GetProperty(jsonProp.Name);
                if (subProp == null || !subProp.CanWrite)  continue; 

                var initialSubPropValue = subProp.GetValue(finalValue);

                var finalSubPropValue = ParseJsonValue(jsonProp.Value, subProp.PropertyType, replaceArray, initialSubPropValue);
                if (finalSubPropValue != null)
                
                    subProp.SetValue(finalValue, finalSubPropValue);
                
            

            return finalValue;
        
    

    static object? CastIEnumrable(List<object?> items, Type target, Type elementType)
    
        object? result = null;

        if (IsList(target))
        
            if (target.IsInterface)
            
                return items;
            
            else
            
                result = Activator.CreateInstance(target);
                var col = (result as IList)!;

                foreach (var item in items)
                
                    col.Add(item);
                
            
        
        else if (target.IsArray)
        
            result = Array.CreateInstance(elementType, items.Count);
            var arr = (result as Array)!;

            for (int i = 0; i < items.Count; i++)
            
                arr.SetValue(items[i], i);
            
        

        return result;
    

    static bool IsList(Type type)
    
       return type.GetInterface("IList") != null;
    

    static Type GetElementType(Type enumerable)
    
        return enumerable.GetInterfaces()
            .First(q => q.IsGenericType && q.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            .GetGenericArguments()[0];
    


用法:

const string Json1 = "\n  \"Title\": \"Startpage\",\n  \"Links\": [\n    \n      \"Id\": 10,\n      \"Text\": \"Start\",\n      \"Link\": \"/index\"\n    ,\n    \n      \"Id\": 11,\n      \"Text\": \"Info\",\n      \"Link\": \"/info\"\n    \n  ]\n";

const string Json2 = "\n  \"Head\": \"Latest news\",\n  \"Links\": [\n    \n      \"Id\": 11,\n      \"Text\": \"News\",\n      \"Link\": \"/news\"\n    ,\n    \n      \"Id\": 21,\n      \"Text\": \"More News\",\n      \"Link\": \"/morenews\"\n    \n  ]\n";

var obj = JsonSerializer.Deserialize<Pages>(Json1)!;

JsonPopulator.PopulateObject(obj, Json2, false);
Console.WriteLine(obj.Links.Count); // 4
Console.WriteLine(JsonSerializer.Serialize(obj));

JsonPopulator.PopulateObject(obj, Json2, true);
Console.WriteLine(obj.Links.Count); // 2
Console.WriteLine(JsonSerializer.Serialize(obj));

【讨论】:

让我问你这个问题:如果要反序列化传入的 JSON 字符串并使用反射解析它,如何判断例如假设 Int 的默认值也是 0,则 Int 实际上设置为 0?许多其他属性类型也是如此。 ...请注意,我不反对反射,已经在其他几个地方使用它。 啊,你做出了正确的评估。我完全忘记了非空默认值。此外,当我说“更好”时,我只关注性能方面。是的,看起来这个更好,尽管它在支持的集合类型的数量方面可能更差(您可能需要编写更多逻辑)。 太好了......现在我们互相了解了:)......我确实删除了我对另一个答案的支持,转而支持这个。 谢谢我也学到了一些东西。没有难受的感觉!【参考方案4】:

经过进一步考虑,我认为更简单的替换解决方案应该是使用 C# Reflection 而不是依赖 JSON。如果它不能满足您的需求,请告诉我:

public class JsonPopulator



    public static void PopulateObjectByReflection(object target, string json, bool replaceArray)
    
        var type = target.GetType();
        var replacements = JsonSerializer.Deserialize(json, type);

        PopulateSubObject(target, replacements, replaceArray);
    

    static void PopulateSubObject(object target, object? replacements, bool replaceArray)
    
        if (replacements == null)  return; 

        var props = target.GetType().GetProperties();

        foreach (var prop in props)
        
            // Skip if can't write
            if (!prop.CanWrite)  continue; 

            // Skip if no value in replacement
            var propType = prop.PropertyType;
            var replaceValue = prop.GetValue(replacements);
            if (replaceValue == GetDefaultValue(propType))  continue; 

            // Now check if it's array AND we do not want to replace it            
            if (replaceValue is IEnumerable<object> replacementList)
            
                var currList = prop.GetValue(target) as IEnumerable<object>;

                
                var finalList = replaceValue;
                // If there is no initial list, or if we simply want to replace the array
                if (currList == null || replaceArray)
                
                    // Do nothing here, we simply replace it
                
                else
                
                    // Append items at the end
                    finalList = currList.Concat(replacementList);

                    // Since casting logic is complicated, we use a trick to just
                    // Serialize then Deserialize it again
                    // At the cost of performance hit if it's too big
                    var listJson = JsonSerializer.Serialize(finalList);
                    finalList = JsonSerializer.Deserialize(listJson, propType);
                

                prop.SetValue(target, finalList);
            
            else if (propType.IsValueType || propType == typeof(string))
            
                // Simply copy value over
                prop.SetValue(target, replaceValue);
            
            else
            
                // Recursively copy child properties
                var subTarget = prop.GetValue(target);
                var subReplacement = prop.GetValue(replacements);

                // Special case: if original object doesn't have the value
                if (subTarget == null && subReplacement != null)
                
                    prop.SetValue(target, subReplacement);
                
                else
                
                    PopulateSubObject(target, replacements, replaceArray);
                
            
        
    

    // From https://***.com/questions/325426/programmatic-equivalent-of-defaulttype
    static object? GetDefaultValue(Type type)
    
        if (type.IsValueType)
        
            return Activator.CreateInstance(type);
        
        return null;
    


使用:

const string Json1 = "\n  \"Title\": \"Startpage\",\n  \"Links\": [\n    \n      \"Id\": 10,\n      \"Text\": \"Start\",\n      \"Link\": \"/index\"\n    ,\n    \n      \"Id\": 11,\n      \"Text\": \"Info\",\n      \"Link\": \"/info\"\n    \n  ]\n";

const string Json2 = "\n  \"Head\": \"Latest news\",\n  \"Links\": [\n    \n      \"Id\": 11,\n      \"Text\": \"News\",\n      \"Link\": \"/news\"\n    ,\n    \n      \"Id\": 21,\n      \"Text\": \"More News\",\n      \"Link\": \"/morenews\"\n    \n  ]\n";

var obj = JsonSerializer.Deserialize<Pages>(Json1)!;

JsonPopulator.PopulateObjectByReflection(obj, Json2, false);
Console.WriteLine(obj.Links.Count); // 4

JsonPopulator.PopulateObjectByReflection(obj, Json2, true);
Console.WriteLine(obj.Links.Count); // 2

当我将 List&lt;Links&gt; 替换为数组 Links[] 时,该解决方案甚至可以工作:

public class Pages

    // ...
    public Links[] Links  get; set; 


JsonPopulator.PopulateObjectByReflection(obj, Json2, false);
Console.WriteLine(obj.Links.Length); // 4

JsonPopulator.PopulateObjectByReflection(obj, Json2, true);
Console.WriteLine(obj.Links.Length); // 2

放弃的解决方案:

我认为一个简单的解决方案是包含父级及其当前属性信息。一个原因是,并非每个IEnumerable 都是可变的(例如数组),因此即使replaceArray 为假,您也需要替换它。

using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;

const string Json1 = @"
    
        ""Bars"": [
             ""Value"": 0 ,
             ""Value"": 1 
        ]
    
";

const string Json2 = @"
    
        ""Bars"": [
             ""Value"": 2 ,
             ""Value"": 3 
        ]
    
";

var foo = JsonSerializer.Deserialize<Foo>(Json1)!;

PopulateObject(foo, Json2, false);
Console.WriteLine(foo.Bars.Count); // 4

PopulateObject(foo, Json2, true);
Console.WriteLine(foo.Bars.Count); // 2

static void PopulateObject(object target, string replacement, bool replaceArray)


    using var doc = JsonDocument.Parse(Json2);
    var root = doc.RootElement;

    PopulateObjectWithJson(target, root, replaceArray, null, null);


static void PopulateObjectWithJson(object target, JsonElement el, bool replaceArray, object? parent, PropertyInfo? parentProp)

    // There should be other checks
    switch (el.ValueKind)
    
        case JsonValueKind.Object:
            // Just simple check here, you may want more logic
            var props = target.GetType().GetProperties().ToDictionary(q => q.Name);

            foreach (var jsonProp in el.EnumerateObject())
            
                if (props.TryGetValue(jsonProp.Name, out var prop))
                
                    var subTarget = prop.GetValue(target);

                    // You may need to check for null etc here
                    ArgumentNullException.ThrowIfNull(subTarget);

                    PopulateObjectWithJson(subTarget, jsonProp.Value, replaceArray, target, prop);
                
            

            break;
        case JsonValueKind.Array:
            var parsedItems = new List<object>();
            foreach (var item in el.EnumerateArray())
            
                // Parse your value here, I will just assume the type for simplicity
                var bar = new Bar()
                
                    Value = item.GetProperty(nameof(Bar.Value)).GetInt32(),
                ;

                parsedItems.Add(bar);
            

            IEnumerable<object> finalItems = parsedItems;
            if (!replaceArray)
            
                finalItems = ((IEnumerable<object>)target).Concat(parsedItems);
            

            // Parse your list into List/Array/Collection/etc
            // You need reflection here as well
            var list = finalItems.Cast<Bar>().ToList();
            parentProp?.SetValue(parent, list);

            break;
        default:
            // Should handle for other types
            throw new NotImplementedException();
    


public class Foo


    public List<Bar> Bars  get; set;  = null!;



public class Bar

    public int Value  get; set; 

【讨论】:

谢谢。我更新了我的问题并添加了要使用的类,如果您可以使用相同的示例更新您的示例,我将不胜感激,以便更容易理解和详细说明。此外,对于您的“JsonValueKind.Array”,它可以很好地保存嵌套对象,我看不出在您的代码示例中将如何处理这些对象。数组项不应该递归传递给“PopulateObjectWithJson”吗?对我来说,项目数组只是另一组对象、数组和值类型。我无法解析已知的列表/数组等类型,因为我不知道它们可能是什么。 @Asons 这里快到早上了(夜猫子我哈哈)所以我可能不会很快检查。正如我在评论中所说,您将需要递归调用该函数,而不是像我的示例那样创建一个对象。我只是想演示如何为您的案例分配数组。希望明天我回来时你已经解决了,如果没有,我可以再试一次(如果我忘记了,请联系我)。 我这里不着急,会等几天,得到更多的答案再开始研究,所以你会有时间更新......而且很好,更完整我经常奖励代码示例 :) 我添加了一个使用反射而不是依赖 JSON 的解决方案。请看看它是否足够好,或者您需要 JSON 解决方案?我想我也可以将此代码添加到您的原始问题中。 没关系。对不起,我最近的工作很忙,所以我不能再跟进这个问题了。希望你找到一个好的解决方案。祝你好运。

以上是关于.NET Core/System.Text.Json:枚举和添加/替换 json 属性/值的主要内容,如果未能解决你的问题,请参考以下文章

.NET平台系列26:在 Windows 上安装 .NET Core/.NET5/.NET6

[.NET大牛之路 005] .NET 的执行模型

ADO.NET和.NET的关系?

VS2022 安装.NET 3.5/.NET 4/.NET 4.5/.NET 4.5.1目标包的方法

.net core 3.0和.net5有什么区别

能说一下ADO.NET 和.NET,还有asp.NET的区别吗?