.Net Core 3.0 JsonSerializer заполняет существующий объект

Я готовлю миграцию с 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 есть несколько дополнительных классов, таких как JsonDocumnet, JsonElement и Utf8JsonReader, хотя я не могу найти ни одного, который принимает существующий объект в качестве параметра.

Я также не достаточно опытен, чтобы увидеть, как использовать существующий.

Там может быть возможная предстоящая функция в .Net Core (спасибо Мустафа Гурсель за ссылку), но пока (а что, если его нет)...

...Теперь мне интересно, можно ли добиться чего-то подобного тому, что можно сделать с PopulateObject?

Я имею в виду, возможно ли с любым другим классом System.Text.Json выполнить то же самое и обновить/заменить только установленные свойства?,... или какой-то другой умный обходной путь?


Вот пример ввода/вывода того, что я ищу, и он должен быть универсальным, поскольку объект, переданный в метод десериализации, имеет тип <T>). У меня есть 2 строки Json для анализа в объект, где у первой установлены некоторые свойства по умолчанию, а у второй - некоторые, например.

Обратите внимание, что значение свойства может быть любого другого типа, кроме string.

JSON-строка 1:

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

JSON-строка 2:

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

Используя 2 строки Json выше, я хочу, чтобы объект приводил к:

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

Как видно из приведенного выше примера, если свойства во 2-м имеют значения/установлены, они заменяют значения в 1-м (как с «Заголовок» и «Ссылка»), если нет, существующее значение сохраняется (как с «Заголовок»)

Комментарии не для расширенного обсуждения; этот разговор был перешел в чат.

Jean-François Fabre 05.07.2019 22:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
47
1
24 149
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Я мало что знаю об этой новой версии плагина, однако я нашел руководство, которому можно следовать учебник с некоторыми примерами

Основываясь на нем, я придумал этот метод и представляю, что он способен решить свою проблему.

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

Будете ли вы всегда получать объект любого вида, всегда ли он будет неопределенным?

Lucas 05.07.2019 15:58

Да, любой объект, нет, иногда неопределенный, иногда нет, что показывает мой образец строки json.

Asons 05.07.2019 16:01

Давайте продолжить обсуждение в чате.

Lucas 05.07.2019 16:20

Вы ищете способ скопировать свойства какого-либо объекта в другой, к JSON это не имеет никакого отношения.

user11523568 05.07.2019 17:33

но цель не в том, чтобы унифицировать json, а в несериализованном объекте

Lucas 05.07.2019 17:47

Не поймите меня неправильно. Ваш ответ наиболее близок к решению. Я имел в виду ОП.

user11523568 05.07.2019 17:52

Ладно, постараюсь максимально улучшить.

Lucas 05.07.2019 17:53

Я вижу пару недостатков. Можем ли мы войти в чат, чтобы обсудить их?

user11523568 05.07.2019 17:54

Спасибо за ваше редактирование. Посмотрим на это позже, в конце баунти-периода.

Asons 05.07.2019 18:35

Если вы уже используете Автокартограф в своем проекте или не возражаете против зависимости от него, вы можете объединить объекты следующим образом:

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 05.07.2019 16:35

он всегда будет получать объект Т-типа, он не может создать для него фиксированный класс

Lucas 05.07.2019 16:53

@SuperPenguino Я предполагаю, что в проекте необходимо объединить определенное количество объектов. Таким образом, должна быть возможность зарегистрировать их при запуске приложения. Даже автоматически по соглашению.

Andrii Litvinov 05.07.2019 16:55

Я не использую AutoMapper, и, если нужно, я лучше придерживаюсь Newtonsoft, который больше подходит для работы. Я хочу/предпочитаю использовать встроенные инструменты для работы с данными Json. И я не понизил голос, так как это может действительно сработать, хотя это не то, что я ищу.

Asons 05.07.2019 17:03

@LGSon Я проверил это перед публикацией, для этого потребуется зарегистрировать все вложенные типы в графе, но это можно сделать автоматически и определенно будет работать. Конечно, если есть возможность, лучше использовать Newtonsoft, особенно если вы не используете AutoMapper.

Andrii Litvinov 05.07.2019 17:13

Я не уверен, решит ли это вашу проблему, но это должно работать как временное решение. Все, что я сделал, это написал простой класс с методом 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"
}

Обратите внимание, что многоуровневый json, вероятно, сломает этот код.

Patrick Mcvay 05.07.2019 17:00

Извините, это не значит, что вы сказали, что я проголосовал против, просто хотел упомянуть об этом (и я удалил этот комментарий сейчас). Спасибо за ваш ответ, с его простотой у него есть свои преимущества, и я посмотрю на него позже.

Asons 05.07.2019 17:21

И кстати, после просмотра других ответов, в качестве исправления многоуровневого json можно было бы проверить объекты тип значения, а если нет IsValueType, то выполнить рекурсивный вызов.

Asons 07.07.2019 11:45

Я понимаю, что могу заставить его поддерживать многоуровневый JSON, но это выходит за рамки вопроса. Я пытался дать самое простое решение проблемы, чтобы не было слишком много беспорядка.

Patrick Mcvay 09.07.2019 21:55

Чем проще/чище, тем лучше, и легкое исправление я могу сделать сам, если это необходимо.

Asons 09.07.2019 23:57
Ответ принят как подходящий

Итак, если предположить, что 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, если тип свойства является ссылочным типом (для этого также потребуется принять Type в качестве параметра в PopulateObject).

Соединив все вместе, мы получим:

void PopulateObject<T>(T target, string jsonSource) where T : class
{
    using var json = JsonDocument.Parse(jsonSource).RootElement;

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

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
    var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
    var parsedValue = JsonSerializer.Deserialize(
        updatedProperty.Value.GetRawText(), 
        propertyType);

    propertyInfo.SetValue(target, parsedValue);
} 

Насколько это надежно? Ну, это, конечно, не сделает ничего разумного для массива JSON, но я не уверен, как вы ожидаете, что метод PopulateObject будет работать с массивом для начала. Я не знаю, насколько он сравним по производительности с версией Json.Net, вам придется проверить это самостоятельно. Он также автоматически игнорирует свойства, не относящиеся к целевому типу, по замыслу. Я думал, что это самый разумный подход, но вы можете подумать иначе, в этом случае свойство null-check должно быть заменено генерацией исключения.

Обновлено:

Я пошел дальше и реализовал глубокую копию:

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

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

void PopulateObject(object target, string jsonSource, Type type)
{
    using var json = JsonDocument.Parse(jsonSource).RootElement;

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

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

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    object parsedValue;

    if (propertyType.IsValueType || propertyType == typeof(string))
    {
        ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
        parsedValue = JsonSerializer.Deserialize(
            updatedProperty.Value.GetRawText(),
            propertyType);
    }
    else
    {
        parsedValue = propertyInfo.GetValue(target);
        P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
        PopulateObject(
            parsedValue, 
            updatedProperty.Value.GetRawText(), 
            propertyType);
    }

    propertyInfo.SetValue(target, parsedValue);
}

Чтобы сделать это более надежным, вам нужно либо иметь отдельный метод PopulateObjectDeep, либо передать PopulateObjectOptions или что-то подобное с флагом deep/shallow.

Обновлено еще раз:

Смысл глубокого копирования в том, что если у нас есть объект

{
    "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 для прямой десериализации JsonElement. Прямо сейчас самое близкое решение — использовать GetRawText(). Я пошел дальше и отредактировал приведенный выше код, чтобы он работал, оставив старую версию зачеркнутой.

Разве глубокое копирование не является излишним для этого варианта использования? Поскольку копируемый объект уже является десериализованным клоном объекта.

user11523568 05.07.2019 18:18

@dfhwze Боюсь, я не понимаю. Вы хотите уточнить?

V0ldek 05.07.2019 19:42

Вместо рекурсивного глубокого копирования всех свойств-потомков достаточно скопировать только дочерние элементы из а в б. Нет причин для глубокого копирования, потому что а — это десериализованный экземпляр, свойства потомков которого не требуют сохранения после копирования в б. Я надеюсь это имеет смысл.

user11523568 05.07.2019 19:46

Но смысл глубокой копии — обновить потомков, а не заменить их. Я добавил редактирование, которое должно прояснить это.

V0ldek 05.07.2019 20:29

Ага! Я понимаю, что вы имеете ввиду. Весь граф исходных объектов может быть частичным подмножеством графа целевых объектов. Это имеет смысл.

user11523568 05.07.2019 21:06

Я награжу вас первоначальной наградой, так как этот ответ дал мне хорошее представление о том, что можно сделать с помощью JsonDocument, что также является основной частью моего вопроса. Спасибо за ваш ответ.

Asons 12.07.2019 14:51

Это не совсем работает сейчас, когда .NET Core 3 вышел из предварительной версии.

ldam 14.10.2019 20:58

@ldam Да, исправил.

V0ldek 14.10.2019 22:19

Привет еще раз. Я столкнулся с проблемой, когда значение объекта может быть массивом (со значениями или объектами). При этом строка foreach (var property in json.EnumerateObject()) с исключением. Как мне перечислить значения массива и получить его объект/значения? ... И то, что я спрашиваю здесь, входит в рамки этого вопроса, или я должен запостить новый?

Asons 01.12.2021 15:02

@Asons Я бы задал отдельный вопрос, чтобы получить больше информации.

V0ldek 01.12.2021 17:46

Я сделал здесь новый вопрос. У меня до сих пор нет правильного ответа, поэтому я надеюсь, что он у вас есть.

Asons 10.12.2021 20:04

Если это только одно использование, и вы не хотите добавлять дополнительные зависимости/много кода, вы не возражаете против небольшой неэффективности а также Я не пропустил что-то очевидное, вы можете просто использовать:

    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 05.07.2019 19:36

Кажется, это разрешено в 3.0 preview6, я думаю, неэффективно проверять наличие дубликатов при анализе. Недостатком является слияние только «верхнего уровня» объектов, поэтому это не удастся, если вам нужно объединить свойство сложного типа/массива между шаблон и Вход.

Peter Wishart 05.07.2019 20:24

Я знаю об этом недостатке ... у меня была такая же проблема с моей собственной версией, тем не менее, я могу с этим смириться, если это сводится к тому, чтобы быть лучшим вариантом.

Asons 05.07.2019 20:31

Вот пример кода, который это делает. Он использует новый Структура 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) производительность может быть улучшена, поскольку она использует Reflection вместо методов MemberAccessor/delegate.

public class JsonPopulator
{
    public void PopulateObject(object obj, string jsonString, JsonSerializerOptions options = null) => PopulateObject(obj, jsonString != null ? Encoding.UTF8.GetBytes(jsonString) : null, options);
    public virtual void PopulateObject(object obj, ReadOnlySpan<byte> jsonData, JsonSerializerOptions options = null)
    {
        options ??= new JsonSerializerOptions();
        var state = new JsonReaderState(new JsonReaderOptions { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling, MaxDepth = options.MaxDepth });
        var reader = new Utf8JsonReader(jsonData, isFinalBlock: true, state);
        new Worker(this, reader, obj, options);
    }

    protected virtual PropertyInfo GetProperty(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
    {
        if (obj == null)
            throw new ArgumentNullException(nameof(obj));

        if (propertyName == null)
            throw new ArgumentNullException(nameof(propertyName));

        var prop = obj.GetType().GetProperty(propertyName);
        return prop;
    }

    protected virtual bool SetPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
    {
        if (obj == null)
            throw new ArgumentNullException(nameof(obj));

        if (propertyName == null)
            throw new ArgumentNullException(nameof(propertyName));

        var prop = GetProperty(ref reader, options, obj, propertyName);
        if (prop == null)
            return false;

        if (!TryReadPropertyValue(ref reader, options, prop.PropertyType, out var value))
            return false;

        prop.SetValue(obj, value);
        return true;
    }

    protected virtual bool TryReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, Type propertyType, out object value)
    {
        if (propertyType == null)
            throw new ArgumentNullException(nameof(reader));

        if (reader.TokenType == JsonTokenType.Null)
        {
            value = null;
            return !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null;
        }

        if (propertyType == typeof(object)) { value = ReadValue(ref reader); return true; }
        if (propertyType == typeof(string)) { value = JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); return true; }
        if (propertyType == typeof(int) && reader.TryGetInt32(out var i32)) { value = i32; return true; }
        if (propertyType == typeof(long) && reader.TryGetInt64(out var i64)) { value = i64; return true; }
        if (propertyType == typeof(DateTime) && reader.TryGetDateTime(out var dt)) { value = dt; return true; }
        if (propertyType == typeof(DateTimeOffset) && reader.TryGetDateTimeOffset(out var dto)) { value = dto; return true; }
        if (propertyType == typeof(Guid) && reader.TryGetGuid(out var guid)) { value = guid; return true; }
        if (propertyType == typeof(decimal) && reader.TryGetDecimal(out var dec)) { value = dec; return true; }
        if (propertyType == typeof(double) && reader.TryGetDouble(out var dbl)) { value = dbl; return true; }
        if (propertyType == typeof(float) && reader.TryGetSingle(out var sgl)) { value = sgl; return true; }
        if (propertyType == typeof(uint) && reader.TryGetUInt32(out var ui32)) { value = ui32; return true; }
        if (propertyType == typeof(ulong) && reader.TryGetUInt64(out var ui64)) { value = ui64; return true; }
        if (propertyType == typeof(byte[]) && reader.TryGetBytesFromBase64(out var bytes)) { value = bytes; return true; }

        if (propertyType == typeof(bool))
        {
            if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True)
            {
                value = reader.GetBoolean();
                return true;
            }
        }

        // fallback here
        return TryConvertValue(ref reader, propertyType, out value);
    }

    protected virtual object ReadValue(ref Utf8JsonReader reader)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.False: return false;
            case JsonTokenType.True: return true;
            case JsonTokenType.Null: return null;
            case JsonTokenType.String: return reader.GetString();

            case JsonTokenType.Number: // is there a better way?
                if (reader.TryGetInt32(out var i32))
                    return i32;

                if (reader.TryGetInt64(out var i64))
                    return i64;

                if (reader.TryGetUInt64(out var ui64)) // uint is already handled by i64
                    return ui64;

                if (reader.TryGetSingle(out var sgl))
                    return sgl;

                if (reader.TryGetDouble(out var dbl))
                    return dbl;

                if (reader.TryGetDecimal(out var dec))
                    return dec;

                break;
        }
        throw new NotSupportedException();
    }

    // we're here when json types & property types don't match exactly
    protected virtual bool TryConvertValue(ref Utf8JsonReader reader, Type propertyType, out object value)
    {
        if (propertyType == null)
            throw new ArgumentNullException(nameof(reader));

        if (propertyType == typeof(bool))
        {
            if (reader.TryGetInt64(out var i64)) // one size fits all
            {
                value = i64 != 0;
                return true;
            }
        }

        // TODO: add other conversions

        value = null;
        return false;
    }

    protected virtual object CreateInstance(ref Utf8JsonReader reader, Type propertyType)
    {
        if (propertyType.GetConstructor(Type.EmptyTypes) == null)
            return null;

        // TODO: handle custom instance creation
        try
        {
            return Activator.CreateInstance(propertyType);
        }
        catch
        {
            // swallow
            return null;
        }
    }

    private class Worker
    {
        private readonly Stack<WorkerProperty> _properties = new Stack<WorkerProperty>();
        private readonly Stack<object> _objects = new Stack<object>();

        public Worker(JsonPopulator populator, Utf8JsonReader reader, object obj, JsonSerializerOptions options)
        {
            _objects.Push(obj);
            WorkerProperty prop;
            WorkerProperty peek;
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.PropertyName:
                        prop = new WorkerProperty();
                        prop.PropertyName = Encoding.UTF8.GetString(reader.ValueSpan);
                        _properties.Push(prop);
                        break;

                    case JsonTokenType.StartObject:
                    case JsonTokenType.StartArray:
                        if (_properties.Count > 0)
                        {
                            object child = null;
                            var parent = _objects.Peek();
                            PropertyInfo pi = null;
                            if (parent != null)
                            {
                                pi = populator.GetProperty(ref reader, options, parent, _properties.Peek().PropertyName);
                                if (pi != null)
                                {
                                    child = pi.GetValue(parent); // mimic ObjectCreationHandling.Auto
                                    if (child == null && pi.CanWrite)
                                    {
                                        if (reader.TokenType == JsonTokenType.StartArray)
                                        {
                                            if (!typeof(IList).IsAssignableFrom(pi.PropertyType))
                                                break;  // don't create if we can't handle it
                                        }

                                        if (reader.TokenType == JsonTokenType.StartArray && pi.PropertyType.IsArray)
                                        {
                                            child = Activator.CreateInstance(typeof(List<>).MakeGenericType(pi.PropertyType.GetElementType())); // we can't add to arrays...
                                        }
                                        else
                                        {
                                            child = populator.CreateInstance(ref reader, pi.PropertyType);
                                            if (child != null)
                                            {
                                                pi.SetValue(parent, child);
                                            }
                                        }
                                    }
                                }
                            }

                            if (reader.TokenType == JsonTokenType.StartObject)
                            {
                                _objects.Push(child);
                            }
                            else if (child != null) // StartArray
                            {
                                peek = _properties.Peek();
                                peek.IsArray = pi.PropertyType.IsArray;
                                peek.List = (IList)child;
                                peek.ListPropertyType = GetListElementType(child.GetType());
                                peek.ArrayPropertyInfo = pi;
                            }
                        }
                        break;

                    case JsonTokenType.EndObject:
                        _objects.Pop();
                        if (_properties.Count > 0)
                        {
                            _properties.Pop();
                        }
                        break;

                    case JsonTokenType.EndArray:
                        if (_properties.Count > 0)
                        {
                            prop = _properties.Pop();
                            if (prop.IsArray)
                            {
                                var array = Array.CreateInstance(GetListElementType(prop.ArrayPropertyInfo.PropertyType), prop.List.Count); // array is finished, convert list into a real array
                                prop.List.CopyTo(array, 0);
                                prop.ArrayPropertyInfo.SetValue(_objects.Peek(), array);
                            }
                        }
                        break;

                    case JsonTokenType.False:
                    case JsonTokenType.Null:
                    case JsonTokenType.Number:
                    case JsonTokenType.String:
                    case JsonTokenType.True:
                        peek = _properties.Peek();
                        if (peek.List != null)
                        {
                            if (populator.TryReadPropertyValue(ref reader, options, peek.ListPropertyType, out var item))
                            {
                                peek.List.Add(item);
                            }
                            break;
                        }

                        prop = _properties.Pop();
                        var current = _objects.Peek();
                        if (current != null)
                        {
                            populator.SetPropertyValue(ref reader, options, current, prop.PropertyName);
                        }
                        break;
                }
            }
        }

        private static Type GetListElementType(Type type)
        {
            if (type.IsArray)
                return type.GetElementType();

            foreach (Type iface in type.GetInterfaces())
            {
                if (!iface.IsGenericType) continue;
                if (iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) return iface.GetGenericArguments()[1];
                if (iface.GetGenericTypeDefinition() == typeof(IList<>)) return iface.GetGenericArguments()[0];
                if (iface.GetGenericTypeDefinition() == typeof(ICollection<>)) return iface.GetGenericArguments()[0];
                if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return iface.GetGenericArguments()[0];
            }
            return typeof(object);
        }
    }

    private class WorkerProperty
    {
        public string PropertyName;
        public IList List;
        public Type ListPropertyType;
        public bool IsArray;
        public PropertyInfo ArrayPropertyInfo;

        public override string ToString() => PropertyName;
    }
}

Я начал 2-ю награду и присужу ваш ответ (нужно подождать 24 часа, пока я не смогу), так как это дало мне хорошее представление о том, что можно сделать с помощью Utf8JsonReader, что также является основной частью моего вопроса. Спасибо за ваш ответ.

Asons 12.07.2019 14:54

Пробовал использовать JsonPopulator, но получил ошибку: «JsonSerializer» не содержит определения для «ReadValue».

tb-mtg 08.11.2019 03:31

@tb-mtg - да, похоже, ReadValue был переименован в Serialize между бета-версией .NET core 3 и финальным выпуском: github.com/dotnet/corefx/commit/… Я обновил свой ответ.

Simon Mourier 08.11.2019 07:56

Я пытался использовать это, но когда я пытаюсь утверждать, что ваш MyClass может быть передан туда и обратно без потери данных, мое утверждение терпит неудачу, потому что значения строковых свойств добавляются в двойные кавычки. См. dotnetfiddle.net/BsAeIu. Исправление похоже на вызов JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); вместо GetRawText(), см. dotnetfiddle.net/Q7SxQp

dbc 16.08.2020 04:04

@dbc - спасибо, что указали на это. Это странно, я на 100% уверен, что тестировал это в начальной версии, так что с тех пор что-то изменилось в классах json. Я обновил код.

Simon Mourier 16.08.2020 08:40

Обходной путь также может быть таким же простым (также поддерживает многоуровневый 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;
        }
    }
}

Выход:

Другие вопросы по теме