.Net Core 3.0 JsonSerializer populate existing object

24,521

Solution 1

So assuming that Core 3 doesn't support this out of the box, let's try to work around this thing. So, what's our problem?

We want a method that overwrites some properties of an existing object with the ones from a json string. So our method will have a signature of:

void PopulateObject<T>(T target, string jsonSource) where T : class

We don't really want any custom parsing as it's cumbersome, so we'll try the obvious approach - deserialize jsonSource and copy the result properties into our object. We cannot, however, just go

T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);

That's because for a type

class Example
{
    int Id { get; set; }
    int Value { get; set; }
}

and a JSON

{
    "Id": 42
}

we will get updateObject.Value == 0. Now we don't know if 0 is the new updated value or if it just wasn't updated, so we need to know exactly which properties jsonSource contains.

Fortunately, the System.Text.Json API allows us to examine the structure of the parsed JSON.

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

We can now enumerate over all properties and copy them.

foreach (var property in json.EnumerateObject())
{
    OverwriteProperty(target, property);
}

We will copy the value using reflection:

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

We can see here that what we're doing is a shallow update. If the object contains another complex object as its property, that one will be copied and overwritten as a whole, not updated. If you require deep updates, this method needs to be changed to extract the current value of the property and then call the PopulateObject recursively if the property's type is a reference type (that will also require accepting Type as a parameter in PopulateObject).

Joining it all together we get:

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

How robust is this? Well, it certainly won't do anything sensible for a JSON array, but I'm not sure how you'd expect a PopulateObject method to work on an array to begin with. I don't know how it compares in performance to the Json.Net version, you'd have to test that by yourself. It also silently ignores properties that are not in the target type, by design. I thought it was the most sensible approach, but you might think otherwise, in that case the property null-check has to be replaced with an exception throw.

EDIT:

I went ahead and implemented a deep copy:

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

To make this more robust you'd either have to have a separate PopulateObjectDeep method or pass PopulateObjectOptions or something similar with a deep/shallow flag.

EDIT 2:

The point of deep-copying is so that if we have an object

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

and populate it with

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

we'd get

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

In case of a shallow copy we'd get Id = 0 in the copied child.

EDIT 3:

As @ldam pointed out, this no longer works in stable .NET Core 3.0, because the API was changed. The Parse method is now Deserialize and you have to dig deeper to get to a JsonElement's value. There is an active issue in the corefx repo to allow direct deserialization of a JsonElement. Right now the closest solution is to use GetRawText(). I went ahead and edited the code above to work, leaving the old version struck-through.

Solution 2

Here is some sample code that does it. It's using the new Utf8JsonReader struct so it populates the object at the same time it parses it. It supports JSON/CLR types equivalence, nested objects (creates if they don't exist), lists and arrays.

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

Note it doesn't support all of what you would probably expect, but you can override or customize it. Things that could be added: 1) naming convention. You'd have to override the GetProperty method. 2) dictionaries or expando objects. 3) performance can be improved because it uses Reflection instead of MemberAccessor/delegate techniques

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

Solution 3

The workaround can also be as simple as this (supports multi-level JSON as well):

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

Output:

enter image description here

Solution 4

I do not know much about this new version of the plug-in, however I found a tutorial that can be followed tutorial with some examples

Based on him I thought of this method and I imagine that he is able to solve his problem

//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://stackoverflow.com/questions/8702603/merging-two-objects-in-c-sharp
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);
    }
}

Solution 5

This code is based on the answer given by V0ldek. It adds the use of custom converters if they are defined on properties. Only properties with public Setter are updated.

  /// <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://stackoverflow.com/a/56906228/3216022"/>
    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;
    }

  }
Share:
24,521

Related videos on Youtube

Asons
Author by

Asons

Worked with business, system and software (desktop/web/database) development since 1991. If you want to get in touch with me, maybe for further assistance and/or hire some help, leave a comment at e.g. a question or answer of mine. 3rd user to get Flexbox Gold badge A couple of useful "dummy image" links https://via.placeholder.com/200x120/599/dee?text=Dummy-Image https://picsum.photos/500/300

Updated on April 01, 2022

Comments

  • Asons
    Asons about 2 years

    I'm preparing a migration from ASP.NET Core 2.2 to 3.0.

    As I don't use more advanced JSON features (but maybe one as described below), and 3.0 now comes with a built-in namespace/classes for JSON, System.Text.Json, I decided to see if I could drop the previous default Newtonsoft.Json.
    Do note, I'm aware that System.Text.Json will not completely replace Newtonsoft.Json.

    I managed to do that everywhere, e.g.

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

    but in one place, where I populate an existing object.

    With Newtonsoft.Json one can do

    JsonConvert.PopulateObject(jsonstring, obj);
    

    The built-in System.Text.Json namespace has some additional classes, like JsonDocumnet, JsonElement and Utf8JsonReader, though I can't find any that take an existing object as a parameter.

    Nor am I experienced enough to see how to make use of the existing one's.

    There might be a possible upcoming feature in .Net Core (thanks to Mustafa Gursel for the link), but meanwhile (and what if it doesn't),...

    ...I now wonder, is it possible to achieve something similar as what one can do with PopulateObject?

    I mean, is it possible with any of the other System.Text.Json classes to accomplish the same, and update/replace only the properties set?,... or some other clever workaround?


    Here is a sample input/output of what I am looking for, and it need to be generic as the object passed into the deserialization method is of type <T>). I have 2 Json string's to be parsed into an object, where the first have some default properties set, and the second some, e.g.

    Note, a property value can be of any other type than a string.

    Json string 1:

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

    Json string 2:

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

    Using the 2 Json strings above, I want an object resulting in:

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

    As seen in above sample, if properties in the 2nd has values/is set, it replace values in the 1st (as with "Head" and "Link"), if not, existing value persist (as with "Title")

    • Jean-François Fabre
      Jean-François Fabre almost 5 years
      Comments are not for extended discussion; this conversation has been moved to chat.
  • Lucas
    Lucas almost 5 years
    Will you always receive an object of any kind, will it always be undefined?
  • Asons
    Asons almost 5 years
    Yes, any kind of object, no, sometimes undefined, sometimes not, which my json string sample show.
  • Lucas
    Lucas almost 5 years
  • Andrii Litvinov
    Andrii Litvinov almost 5 years
    Care to explain down-vote? Obviously the functionality is not yet implemented in .NET Core 3.0. So there are two approaches basically: either to create some custom implementation or leverage existing tool that can do the job.
  • Lucas
    Lucas almost 5 years
    it will always receive a T-type object, it can not create a fixed class for it
  • Andrii Litvinov
    Andrii Litvinov almost 5 years
    @SuperPenguino I assume there is a defined number of objects that need to be merged in the project. So it should be possible to register them at the app startup. Even automatically by convention.
  • Patrick Mcvay
    Patrick Mcvay almost 5 years
    Please note that multi-level json would probably break this code.
  • Asons
    Asons almost 5 years
    I don't use AutoMapper, and if needed, I better stick to Newtonsoft being more appropriate for the job. What I want/prefer is to use the built-in tools dealing with Json data. And I didn't downvote as this might actually work, though is not what I am looking for.
  • Andrii Litvinov
    Andrii Litvinov almost 5 years
    @LGSon I tested it before posting, it will require registering all nested types in a graph, but it can be done automatically and will definitely work. Of course it is better to stick with Newtonsoft if that's an option, especially if you don't use AutoMapper.
  • Asons
    Asons almost 5 years
    Sorry, didn't meant you said I did downvote, just wanted to mention that (and I removed that comment now). Thanks for your answer, with is simplicity it has its benefits, and I will have a look at it later.
  • Admin
    Admin almost 5 years
    You are looking for a way to copy properties of any object into another, this has nothing to do with JSON.
  • Lucas
    Lucas almost 5 years
    but the objective is not to unify the json, but the unserialized object
  • Admin
    Admin almost 5 years
    Don't get me wrong. Your answer is the closest to a solution. I was referring to the OP.
  • Lucas
    Lucas almost 5 years
    Okay, I'm trying to improve it as much as possible.
  • Admin
    Admin almost 5 years
    I can see a couple of shortcomings. Could we go into chat to discuss them?
  • Admin
    Admin almost 5 years
    Isn't deep copy overkill for this use case? Since the object to copy is already a deserialised clone of the object.
  • Asons
    Asons almost 5 years
    Thanks for your edit. Will have a look at it later, in the end of the bounty period.
  • Asons
    Asons almost 5 years
    Thanks, will check later to see if the JsonSerializer allow the same property twice in the json string. I did like this myself at first, before I found Newtonsoft's method. What might not be clear in my question is that I do both the template and input deserialization as the same time, hence this trick is less inefficient than it appears, as I don't need the extra JsonSerializer.ToString();. I also cache the result and doesn't run it unless any of the strings is edited, which makes it even less of an issue with a little inefficiency.
  • V0ldek
    V0ldek almost 5 years
    @dfhwze I'm afraid I don't understand. You want to elaborate?
  • Admin
    Admin almost 5 years
    Rather than recursively deep-copying all descendant properties, it would suffice to only copy the children from a to b. There is no reason to deep-copy, because a is a deserialised instance of which the descendant properties don't require to live on after the copy to b. I hope this makes sense.
  • Peter Wishart
    Peter Wishart almost 5 years
    It does seem to be allowed in 3.0 preview6, I guess its inefficient to check for duplicates as it parses. The drawback is it's only merging the "top level" of the objects so would fail if you need to merge a complex type / array property between template and input.
  • V0ldek
    V0ldek almost 5 years
    But the point of the deep copy is to update the descendants instead of replacing them. I've added an edit that's supposed to clarify that.
  • Asons
    Asons almost 5 years
    Am aware of that drawback...had the same issue with my own version, still, can live with it if it comes down to being the best option.
  • Admin
    Admin almost 5 years
    Aha! I see what you mean. The entire source object graph can be a partial subset of the target object graph. This makes sense.
  • Asons
    Asons almost 5 years
    And btw, after looking through the other answers, as somewhat fix to multi-level json would be to check the objects value type, and if not IsValueType then do a recursive call.
  • Patrick Mcvay
    Patrick Mcvay almost 5 years
    I realize that I could make it support multi level JSON, but that is out of the scope of the question. I was trying to give the most simple fix for the problem at hand, so there wouldn't be entirely too much clutter.
  • Asons
    Asons almost 5 years
    The simpler/cleaner, the better, and an easy fix I can do myself if needed.
  • Asons
    Asons almost 5 years
    I will award you the initial bounty, as this answer gave me a good picture of what one can do using JsonDocument, which also where a main part of my question. Thank you for your answer.
  • Asons
    Asons almost 5 years
    I started a 2nd bounty, and will award your answer it (need to wait 24 hours until I can), as it gave me a good picture of what one can do using Utf8JsonReader, which also where a main part of my question. Thank you for your answer.
  • ldam
    ldam over 4 years
    This doesn't quite work now that .NET Core 3 is out of preview.
  • V0ldek
    V0ldek over 4 years
    @ldam Yep, fixed that.
  • tb-mtg
    tb-mtg over 4 years
    Tried using JsonPopulator but I got error: 'JsonSerializer' does not contain a definition for 'ReadValue'
  • Simon Mourier
    Simon Mourier over 4 years
    @tb-mtg - yes, it looks like ReadValue was renamed into Serialize between .NET core 3 beta and the final release: github.com/dotnet/corefx/commit/… I have updated my answer.
  • dbc
    dbc over 3 years
    I tried using this, but when I attempt to assert that your MyClass can be round-tripped without data loss, my assertion fails because the values of the string properties are getting double-quotes added. See dotnetfiddle.net/BsAeIu. Fix seems to be to call JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); instead of GetRawText(), see dotnetfiddle.net/Q7SxQp
  • Simon Mourier
    Simon Mourier over 3 years
    @dbc - thanks for pointing that out. That's weird, I'm 100% sure I've tested this in the initial version, so something has changed from then in the json classes. I've updated the code.
  • Asons
    Asons over 2 years
    Hi again. I ran into an issue where an object value can be an array (with values or objects). With that the line foreach (var property in json.EnumerateObject()) with throw an exception. How would I enumerate an array's values, and get its object/values? ... And is what I ask here within the boundary of this question, or should I post a new one?
  • V0ldek
    V0ldek over 2 years
    @Asons I'd ask as a separate question to get more visibility.
  • Asons
    Asons over 2 years
    I did here new question. I still haven't got a proper answer yet, so I hope you have one.