.NET Core 3.0 JsonSerializer 如何填充现有对象?

49

我正在准备从ASP.NET Core 2.2迁移到3.0。

由于我不使用更高级的JSON功能(但可能会用到下面描述的一个),而3.0现在内置了JSON的命名空间/类,System.Text.Json,所以我决定看看能否放弃以前的默认选项Newtonsoft.Json
请注意,我知道System.Text.Json不会完全取代Newtonsoft.Json

我已经成功地在每个地方做到了这一点,例如:

var obj = JsonSerializer.Parse<T>(jsonstring);

var jsonstring = JsonSerializer.ToString(obj);

但是在一个地方,我要填充一个已有的对象。

使用Newtonsoft.Json,可以这样做:

JsonConvert.PopulateObject(jsonstring, obj);

内置的System.Text.Json命名空间有一些其他的类,例如JsonDocumentJsonElementUtf8JsonReader,但我找不到任何一个以现有对象作为参数的类。

而且我没有足够的经验来看出如何利用现有的类。

可能会在 .Net Core 中出现一种可能的新功能(感谢Mustafa Gursel提供的链接),但同时(如果没有这个功能),...

...我现在想知道,是否可以实现与PopulateObject相似的功能?

我的意思是,是否可以使用任何其他的System.Text.Json类来完成同样的工作,并仅更新/替换已设置的属性?或者使用其他巧妙的解决方法?


以下是我所寻求的样例输入/输出,它需要具有通用性,因为传递到反序列化方法中的对象是类型为<T>的。我有2个Json字符串要解析为一个对象,其中第一个设置了一些默认属性,第二个设置了一些属性。

注意,属性值可以是任何类型而不仅仅是string

Json字符串1:

{
  "Title": "Startpage",
  "Link": "/index",
}

Json字符串2:

{
  "Head": "Latest news"
  "Link": "/news"
}

使用上述两个JSON字符串,我想要一个结果为对象:

{
  "Title": "Startpage",
  "Head": "Latest news",
  "Link": "/news"
}

如上示例所示,如果第二个属性具有值/已设置,则它将替换第一个属性的值(例如"Head"和"Link"),如果没有,则现有值保持不变(例如"Title")。


评论不能作为长时间讨论的场所;此对话已经移至聊天室 - Jean-François Fabre
8个回答

19
假设Core 3不支持此功能,让我们尝试解决这个问题。那么,我们的问题是什么?
我们需要一种方法,可以使用来自JSON字符串的属性覆盖现有对象的某些属性。因此,我们的方法将具有以下签名:
void PopulateObject<T>(T target, string jsonSource) where T : class

我们不希望进行任何自定义解析,因为这会很麻烦,所以我们将尝试明显的方法——反序列化jsonSource并将结果属性复制到我们的对象中。但是我们不能只去{{这样做}}。
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);
} 

我们可以看到这里我们正在进行的是一个浅层更新。如果对象包含另一个复杂对象作为其属性,那么该对象将被整体复制并覆盖,而不是更新。如果您需要深度更新,则需要更改此方法以提取属性的当前值,然后在属性类型为引用类型时递归调用 PopulateObject(这也需要在 PopulateObject 中接受 Type 作为参数)。
将所有内容组合在一起,我们得到:
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版本相比如何,您需要自己测试。出于设计考虑,它还会默默地忽略目标类型中不存在的属性。我认为这是最明智的方法,但您可能持有不同看法,在这种情况下,属性空值检查必须替换为异常抛出。 编辑:
我继续实现了深层复制:
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或类似选项。
编辑2:
深度复制的目的是,如果我们有一个对象
{
    "Id": 42,
    "Child":
    {
        "Id": 43,
        "Value": 32
    },
    "Value": 128
}

并使用{{}}填充它

{
    "Child":
    {
        "Value": 64
    }
}

我们需要

{
    "Id": 42,
    "Child":
    {
        "Id": 43,
        "Value": 64
    },
    "Value": 128
}

在浅拷贝情况下,我们会在被复制的子对象中得到Id = 0编辑3: 正如@ldam所指出的那样,在稳定的.NET Core 3.0中,这种方法已经不再适用,因为API已经改变。现在Parse方法是Deserialize,你必须深入挖掘才能获得JsonElement的值。在corefx repo中有一个活跃的问题允许直接反序列化JsonElement。目前最接近的解决方案是使用GetRawText()。我已经编辑了上面的代码使其工作,并将旧版本划掉。

1
啊哈!我明白你的意思了。整个源对象图可以是目标对象图的部分子集。这很有道理。 - user11523568
1
我将授予您初始赏金,因为这个答案给了我一个很好的图片,说明了使用JsonDocument可以做什么,这也是我的问题的主要部分。感谢您的回答。 - Asons
1
现在 .NET Core 3 已经不再是预览版,所以这个方法并不能完全正常工作。 - ldam
3
没问题,已修复。 - V0ldek
再次您好。我遇到了一个问题,对象的值可能是一个数组(带有值或对象)。这样,行foreach(var property in json.EnumerateObject()) 将会抛出异常。我该如何枚举数组的值,并获取其对象/值?还有我在这里提问是否符合此问题的范围,或者我应该发布一个新的问题? - Asons
显示剩余6条评论

10

以下是一些示例代码,它使用新的Utf8JsonReader结构体,因此它会在解析的同时填充对象。它支持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)字典或动态对象。3)性能可以提高,因为它使用反射而不是MemberAccessor /委托技术。

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;
    }
}

1
我开始了第二个赏金任务,并且会将它授予你的答案(需要等待24小时),因为它给了我一个使用Utf8JsonReader的好方法,这也是我的问题的主要部分。感谢你的回答。 - Asons
尝试使用JsonPopulator,但是出现了错误:“JsonSerializer”不包含“ReadValue”的定义。 - tb-mtg
1
@tb-mtg - 是的,看起来ReadValue在.NET core 3 beta和最终版本之间被重命名为Serialize:https://github.com/dotnet/corefx/commit/32776a3e25aa5b47c96ea2dd588c2201ca18a048#diff-0a0dfba1de7fd4b090bd53ce98eeeb58 我已经更新了我的答案。 - Simon Mourier
1
我尝试使用这个,但是当我试图断言你的 MyClass 可以在不丢失数据的情况下进行往返转换时,我的断言失败了,因为字符串属性的值被添加了双引号。请参见 https://dotnetfiddle.net/BsAeIu。修复方法似乎是调用 JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); 而不是 GetRawText(),请参见 https://dotnetfiddle.net/Q7SxQp。 - dbc
1
@dbc - 感谢您指出这一点。很奇怪,我百分之百确定我在最初的版本中测试过这个,所以从那时起json类中有些东西已经改变了。我已经更新了代码。 - Simon Mourier

7
解决方法也可以这么简单(还支持多级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;
        }
    }
}

输出:

在此输入图片描述


2
我不太了解这个插件的新版本,但我找到了一个可以遵循的教程:教程和示例 根据他的方法,我想到了这种解决问题的方式。
//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: https://dev59.com/_Goy5IYBdhLWcg3wN7UX
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);
    }
}

你是否总是会收到任何类型的对象,或者它总是未定义的? - Lucas
是的,任何类型的对象,不确定的情况有时会出现,我的 JSON 字符串样本就显示了这一点。 - Asons
让我们在聊天中继续这个讨论 - Lucas
但目标不是统一JSON,而是反序列化对象。 - Lucas
不要误会,你的答案是最接近解决方案的。我指的是原帖作者。 - user11523568
显示剩余4条评论

1

这段代码基于V0ldek的答案,如果属性定义了自定义转换器,则它会添加使用这些转换器的功能。只有具有公共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 StackOverflow</remarks>
    /// <see cref="https://dev59.com/0FMI5IYBdhLWcg3wLYbE#56906228"/>
    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;
    }

  }

0

如果您的项目已经使用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; }
}

能否解释一下为什么要点踩?显然,在 .NET Core 3.0 中还没有实现这个功能。因此,基本上有两种方法:要么创建一些自定义实现,要么利用现有的工具来完成任务。 - Andrii Litvinov
它将始终接收一个T类型的对象,不能为其创建固定的类。 - Lucas
@SuperPenguino 我假设项目中需要合并的对象数量是确定的。因此,在应用程序启动时注册它们应该是可能的。甚至可以通过约定自动完成。 - Andrii Litvinov
我不使用 AutoMapper,如果需要的话,我更倾向于使用更适合此工作的 Newtonsoft。我想要/更喜欢使用处理 Json 数据的内置工具。虽然这可能有效,但并非我所寻找的,因此我没有投反对票。 - Asons
@LGSon 在发布前我已经测试过了,它需要在图中注册所有嵌套类型,但是这可以自动完成且一定有效。当然,如果有可能最好还是使用Newtonsoft,尤其是如果您不使用AutoMapper的话。 - Andrii Litvinov

0

我不确定这是否能解决你的问题,但它应该可以作为一个临时解决方法。我所做的只是编写了一个简单的类,并在其中编写了一个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"
}

1
请注意,多层级的JSON可能会破坏此代码。 - Patrick Mcvay
抱歉,我并不是说你给我点了踩,只是想提一下(现在已经删除了那条评论)。感谢你的回答,简单的方法确实有其好处,我稍后会仔细研究。 - Asons
顺便说一下,在查看其他答案后,对于多级JSON的某种修复方法是检查对象的_value类型_,如果不是IsValueType,则进行递归调用。 - Asons
我意识到我可以让它支持多级JSON,但这超出了问题的范围。我试图为手头的问题提供最简单的解决方案,以便不会有太多的混乱。 - Patrick Mcvay
越简单/清晰越好,如果需要的话我可以自己轻松修复。 - Asons

0

如果只是一次使用,并且您不想添加额外的依赖项/大量代码,您也不介意有些低效,并且我没有遗漏任何明显的内容,您可以直接使用:

    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);
    }

谢谢,稍后会检查一下JsonSerializer是否允许在json字符串中出现相同的属性两次。我自己一开始也是这样做的,直到我找到了Newtonsoft的方法。可能我的问题并不清楚,我同时进行模板和输入反序列化,因此这种技巧比看起来的效率更低,因为我不需要额外的JsonSerializer.ToString();。我还缓存了结果,并且只有在任何一个字符串被编辑时才运行它,这使得它即使具有一点低效性也不成问题。 - Asons
在3.0 preview6中似乎允许这样做,我猜查找重复项的效率不高,因为需要解析。缺点是它只能合并对象的“顶层”,因此如果您需要在templateinput之间合并复杂类型/数组属性,则会失败。 - Peter Wishart
我知道那个缺点...我的版本也有同样的问题,但如果它是最好的选择,我还是可以接受的。 - Asons

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接