2 votes

NET Core/System.Text.Json : Enumérer et ajouter/remplacer des propriétés/valeurs json

Dans une question précédente, j'ai demandé comment faire pour alimenter un objet existant using System.Text.Json.

Une des grandes réponses a montré une solution en analysant la chaîne json avec JsonDocument et l'énumérer avec EnumerateObject .

Au fil du temps, ma chaîne json a évolué et contient maintenant un tableau d'objets, et lorsqu'elle est analysée avec le code de la réponse liée, elle lance l'exception suivante :

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

J'ai compris que l'on peut, d'une manière ou d'une autre, rechercher les JsonValueKind.Array et de faire quelque chose comme ceci

if (json.ValueKind.Equals(JsonValueKind.Array))
{
    foreach (var item in json.EnumerateArray())
    {
        foreach (var property in item.EnumerateObject())
        {
            await OverwriteProperty(???);
        }
    }
}

mais je n'arrive pas à le faire fonctionner.

Comment procéder et quelle est la solution générique ?

Je souhaite obtenir "Résultat 1 où les éléments du tableau sont ajoutés/mise à jour, et "Résultat 2 (lors du passage d'une variable), où le tableau entier est remplacé.

Pour "Résultat 2 Je suppose que l'on peut détecter if (JsonValueKind.Array)) dans le OverwriteProperty et où/comment passer la variable "replaceArray" ? ... pendant l'itération du tableau ou des objets ?

Quelques exemples de données :

Chaîne Json initiale

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

Chaîne Json à ajouter/mettre à jour

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

Résultat 1

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

Résultat 2

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

Classes

public class Pages
{
    public string Title { get; set; }
    public string Head { get; set; }
    public List<Links> Links { get; set; }
}

public class Links
{
    public int Id { get; set; }
    public string Text { get; set; }
    public string Link { get; set; }
}

Code C# :

public async Task PopulateObjectAsync(object target, string source, Type type, bool replaceArrays = false)
{
    var json = JsonDocument.Parse(source).RootElement;

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

    return;
}

public async Task OverwriteProperty(object target, JsonProperty updatedProperty, Type type, bool replaceArrays)
{
    var propertyInfo = type.GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    object parsedValue;

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

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

    propertyInfo.SetValue(target, parsedValue);
}

1voto

DatVM Points 2160

C'est au cas où vous voudriez utiliser la solution JSON uniquement, même si je pense qu'elle n'est pas tellement meilleure que la solution Reflection. Elle couvre absolument moins de cas d'utilisation que la solution par défaut JsonSerializer Par exemple, vous pouvez avoir des problèmes avec IReadOnlyCollection s.

public class JsonPopulator
{

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

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

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

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

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

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

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

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

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

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

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

            var finalValue = initialValue;

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

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

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

                var initialSubPropValue = subProp.GetValue(finalValue);

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

            return finalValue;
        }
    }

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

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

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

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

        return result;
    }

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

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

}

Utilisation :

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

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

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

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

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

0voto

DatVM Points 2160

Après mûre réflexion, je pense qu'une solution de remplacement plus simple consisterait à utiliser C# Reflection au lieu de s'appuyer sur JSON. Dites-moi si cela ne répond pas à vos besoins :

public class JsonPopulator
{

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

        PopulateSubObject(target, replacements, replaceArray);
    }

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

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

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

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

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

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

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

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

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

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

Utilisation :

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

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

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

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

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

La solution fonctionne même lorsque je remplace List<Links> avec tableau Links[] :

public class Pages
{
    // ...
    public Links[] Links { get; set; }
}

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

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

Solution abandonnée :

Je pense qu'une solution simple consisterait à inclure le parent et ses informations de propriété actuelles. L'une des raisons est que tous les IEnumerable est de toute façon mutable (Array par exemple), vous voudrez donc le remplacer par replaceArray étant faux.

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

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

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

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

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

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

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

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

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

static void PopulateObjectWithJson(object target, JsonElement el, bool replaceArray, object? parent, PropertyInfo? parentProp)
{
    // There should be other checks
    switch (el.ValueKind)
    {
        case JsonValueKind.Object:
            // Just simple check here, you may want more logic
            var props = target.GetType().GetProperties().ToDictionary(q => q.Name);

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

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

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

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

                parsedItems.Add(bar);
            }

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

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

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

public class Foo
{

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

}

public class Bar
{
    public int Value { get; set; }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X