我必须为我从外部API获得的动态JSON创建一个C#类.json看起来像这样:

{
    "id": "dataset0",
    "properties": {
        "createtime": "datetime format"
    },
    "dynamic0": {
        "id": "123-456",
        "metadata": {
            "metadata0": "value0",
            "metadata1": "value1"
        }
    },
    "dynamic1": {
        "id": "456-789",
        "metadata": {
            "metadata0": "value0"
        }
    }
}

—id属性是固定的,后面是0..>& n个具有固定 struct 的动态键.

我的 idea 是:继承字典,并通过两个固定的属性增强它.所以我创建了一个这样的类:

public class Dataset<TKey, TValue> : IDictionary<TKey, TValue> where TValue : DynamicKey where TKey : notnull
{
    public string Id { get; set; } = string.Empty;
    public Properties { get; set; } = new Properties ();

    private readonly Dictionary<TKey, TValue> data = new();

    public TValue this[TKey key]
    {
        get => data[key]; 
        set => data[key] = value;
    }

    public ICollection<TKey> Keys => data.Keys;

    public ICollection<TValue> Values => data.Values;

    public int Count => data.Count;

    public bool IsReadOnly => throw new NotImplementedException();

    public void Add(TKey key, TValue value)
    {
        data.Add(key, value);
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        data.Add(item.Key, item.Value);
    }

    public void Clear()
    {
        data.Clear();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return data.Contains(item);
    }

    public bool ContainsKey(TKey key)
    {
        return data.ContainsKey(key);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        throw new NotImplementedException();
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return data.GetEnumerator();
    }

    public bool Remove(TKey key)
    {
        return data.Remove(key);
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        return data.Remove(item.Key);
    }

    public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
    {
        return data.TryGetValue(key, out value);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

使用这种方法,它直接在C#中工作完全正常,所以我可以随心所欲地使用它:

var data = new Dataset<string, DynamicKey>();
data.Id = "test";
data.Properties = new Properties
{
    CreateTime = DateTime.Now
};
data.Add("test", new DynamicKey { Id = "123456", Metadata = new Dictionary<string, object> { { "some", "metadata" } } });

但是它不能(正确地)(反)序列化从/到json,

JsonConvert.SerializeObject(data); -> returns only the dict elements, but the fixed properties are missing
JsonConvert.DeserializeObject<Dataset<string, DynamicKey>>(json) -> try to parsed fixed properties into dict elements and failes them

有什么解决办法吗?

我也试过:

  • Newtonsoft(反)串行化器
  • System.Text.Json(反)序列化器
  • [JsonExtensionData]属性作为替代方法

推荐答案

你的情况和100号的情况基本相同. 你的根JSON对象有一组固定的属性,然后是0.. n个具有固定 struct 的动态名称的属性.因此,您可以使用来自this answer的转换器将JSON读入包含Dictionary<TKey, TValue> DynamicValues { get; init; }属性的模型,以捕获动态命名的属性. 来自该答案的转换器需要稍微调整,以考虑到根模型具有泛型参数这一事实.

首先定义以下数据模型:

[JsonConverter(typeof(TypedExtensionDataConverter))]
public class Root<TKey, TValue> where TValue : DynamicKey where TKey : notnull
{
    // The fixed properties from the JSON:
    public string id { get; set; }
    public Properties properties { get; set; }

    // A dictionary to hold the properties with dynamic names but a fixed structure in the JSON:
    [JsonTypedExtensionData]
    public Dictionary<TKey, TValue> DynamicValues { get; init; } = new();
}

public class Properties // Properties was not shown in your question
{
    public string createtime { get; set; }
}

public abstract class DynamicKey; // DynamicKey was not shown in your question

public class IdAndMetadata : DynamicKey
{
    public string id { get; set; }
    public Metadata metadata { get; set; }
}

public class Metadata
{
    public string metadata0 { get; set; }
    public string metadata1 { get; set; }
}

并介绍以下转换器:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class JsonTypedExtensionDataAttribute : Attribute;

public class TypedExtensionDataConverter : JsonConverter
{
    // TypedExtensionDataConverter taken from this answer https://stackoverflow.com/a/40094403/3744182
    // to https://stackoverflow.com/questions/40088941/how-to-deserialize-a-child-object-with-dynamic-numeric-key-names
    // and adjusted so it can be applied to a class with a generic parameter.

    public override bool CanConvert(Type objectType) => throw new NotImplementedException(); // CanConvert() is not called when the converter is applied via attributes

    static JsonProperty GetExtensionJsonProperty(JsonObjectContract contract)
    {
        try
        {
            return contract.Properties.Where(p => p.PropertyName != null && p.AttributeProvider?.GetAttributes(typeof(JsonTypedExtensionDataAttribute), false).Any() == true).Single();
        }
        catch (InvalidOperationException ex)
        {
            throw new JsonSerializationException(string.Format("Exactly one property with JsonTypedExtensionDataAttribute is required for type {0}", contract.UnderlyingType), ex);
        }
    }

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        if (!(serializer.ContractResolver.ResolveContract(objectType) is JsonObjectContract contract))
            throw new JsonSerializationException($"Contract for {objectType} is not a JsonObjectContract");
        if (!(contract.DefaultCreator is {} creator))
            throw new JsonSerializationException($"No default creator found for {objectType}");
        var extensionJsonProperty = GetExtensionJsonProperty(contract);

        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;

        var jObj = JObject.Load(reader);

        var extensionJProperty = (JProperty?)null;
        for (int i = jObj.Count - 1; i >= 0; i--)
        {
            var property = (JProperty)jObj.AsList()[i];
            if (contract.Properties.GetClosestMatchProperty(property.Name) == null)
            {
                if (extensionJProperty == null)
                {
                    extensionJProperty = new JProperty(extensionJsonProperty.PropertyName!, new JObject());
                    jObj.Add(extensionJProperty);
                }
                ((JObject)extensionJProperty.Value).Add(property.RemoveFromLowestPossibleParent());
            }
        }

        var value = existingValue ?? creator();
        using (var subReader = jObj.CreateReader())
            serializer.Populate(subReader, value);
        return value;
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        if (value == null)
        {
            writer.WriteNull();
            return;
        }
        if (!(serializer.ContractResolver.ResolveContract(value.GetType()) is JsonObjectContract contract))
            throw new JsonSerializationException($"Contract for {value.GetType()} is not a JsonObjectContract");
        var extensionJsonProperty = GetExtensionJsonProperty(contract);

        JObject jObj;
        using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
        {
            jObj = JObject.FromObject(value!, serializer);
        }

        var extensionValue = (jObj[extensionJsonProperty.PropertyName!] as JObject)?.RemoveFromLowestPossibleParent();
        if (extensionValue != null)
            for (int i = extensionValue.Count - 1; i >= 0; i--)
            {
                var property = (JProperty)extensionValue.AsList()[i];
                jObj.Add(property.RemoveFromLowestPossibleParent());
            }

        jObj.WriteTo(writer);
    }

    [ThreadStatic]
    static bool disabled;

    // Disables the converter in a thread-safe manner.
    bool Disabled { get { return disabled; } set { disabled = value; } }

    public override bool CanWrite => !Disabled;

    public override bool CanRead => !Disabled;
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        ArgumentNullException.ThrowIfNull(reader);
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        ArgumentNullException.ThrowIfNull(reader);
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }

    public static TJToken? RemoveFromLowestPossibleParent<TJToken>(this TJToken? node) where TJToken : JToken
    {
        if (node == null)
            return null;
        JToken toRemove;
        var property = node.Parent as JProperty;
        if (property != null)
        {
            // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
            toRemove = property;
            property.Value = null!;
        }
        else
        {
            toRemove = node;
        }
        if (toRemove.Parent != null)
            toRemove.Remove();
        return node;
    }
    
    public static IList<JToken> AsList(this IList<JToken> container) => container;
}

internal struct PushValue<T> : IDisposable
{
    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    {
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    }

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose() => setValue?.Invoke(oldValue);
}

现在,您将能够成功地将问题中的JSON序列化和重新序列化:

var json = 
    """
    {
        "id": "dataset0",
        "properties": {
            "createtime": "datetime format"
        },
        "dynamic0": {
            "id": "123-456",
            "metadata": {
                "metadata0": "value0",
                "metadata1": "value1"
            }
        },
        "dynamic1": {
            "id": "456-789",
            "metadata": {
                "metadata0": "value0"
            }
        }
    }
    """;

var root = JsonConvert.DeserializeObject<Root<string, IdAndMetadata>>(json);
var settings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
};
var json2 = JsonConvert.SerializeObject(root, Formatting.Indented, settings);

注:

here.第here

Csharp相关问答推荐

如何将ref T*重新解释为ref nint?

使用变量子根名称在C#中重新初始化SON文件

在依赖性注入和继承之间进行 Select

Monty Hall游戏节目模拟给我50/50的结果

System. InvalidOperationException:无法将数据库中的字符串值i转换为映射的ItemType枚举中的任何值''''

属性getter和setter之间的空性不匹配?

返回TyedResults.BadRequest<;字符串>;时问题详细信息不起作用

内部接口和类的DI解析

获取具有AutoFaces的所有IOptions对象的集合

如何使用XmlSerializer序列化带有CDATA节的XML文件?

如何在VS代码中为C#DotNet配置.json选项以调试内部终端的控制台应用程序

什么时候接受(等待)信号灯?尽可能的本地化?

DbContext-传递自定义配置选项

在C#中有没有办法减少大型数组中新字符串的分配?

如何强制新设置在Unity中工作?

JSON串行化程序问题.SQLite中的空值

获取应用程序版本信息时出现奇怪信息

嵌套Blazor组件内的验证

我是否以错误的方式使用了异步延迟初始化?

无法通过服务控制台启动.NET Core 6.0服务(错误1053)