让.NET Core JsonSerializer序列化私有成员

10

我有一个带有私有List<T>属性的类,我想使用JsonSerializer进行序列化/反序列化。在.NET Core中似乎不支持使用JsonPropertyAttribute。那么如何将我的私有列表属性进行序列化?

我正在使用System.Text.Json来实现此功能。


你使用的是Newtonsoft.Json还是System.Text.Json? - Heretic Monkey
序列化但未反序列化?添加一个只读的公共属性,在获取时返回您的私有列表。 - user3456014
@HereticMonkey 我正在使用 System.Text.Json。已将此添加到问题中。 - Mats
2
System.Text.Json默认不支持内部和私有的Getter和Setter。详情请参考:https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#internal-and-private-property-setters-and-getters - Pavel Anikhouski
1
请注意:https://github.com/dotnet/runtime/pull/34675 - crgolden
4个回答

11

看起来System.Text.Json不支持私有属性序列化。

https://learn.microsoft.com/tr-tr/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#internal-and-private-property-setters-and-getters

但正如微软的文档所说,您可以使用自定义转换器来完成此操作。

https://www.thinktecture.com/en/asp-net/aspnet-core-3-0-custom-jsonconverter-for-the-new-system_text_json/

序列化和反序列化的代码片段;

  public class Category
    {
        public Category(List<string> names)
        {
            this.Names1 = names;
        }

        private List<string> Names1 { get; set; }
        public string Name2 { get; set; }
        public string Name3 { get; set; }
    }


 public class CategoryJsonConverter : JsonConverter<Category>
    {
        public override Category Read(ref Utf8JsonReader reader,
                                      Type typeToConvert,
                                      JsonSerializerOptions options)
        {
                       var name = reader.GetString();

            var source = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(name);

            var category = new Category(null);

            var categoryType = category.GetType();
            var categoryProps = categoryType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

            foreach (var s in source.Keys)
            {
                var categoryProp = categoryProps.FirstOrDefault(x => x.Name == s);

                if (categoryProp != null)
                {
                    var value = JsonSerializer.Deserialize(source[s].GetRawText(), categoryProp.PropertyType);

                    categoryType.InvokeMember(categoryProp.Name,
                        BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance,
                        null,
                        category,
                        new object[] { value });
                }
            }

            return category;
        }

        public override void Write(Utf8JsonWriter writer,
                                   Category value,
                                   JsonSerializerOptions options)
        {
            var props = value.GetType()
                             .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                             .ToDictionary(x => x.Name, x => x.GetValue(value));

            var ser = JsonSerializer.Serialize(props);

            writer.WriteStringValue(ser);
        }
    }

static void Main(string[] args)
    {
        Category category = new Category(new List<string>() { "1" });
        category.Name2 = "2";
        category.Name3 = "3";

        var opt = new JsonSerializerOptions
        {
            Converters = { new CategoryJsonConverter() },
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };

        var json = JsonSerializer.Serialize(category, opt);

        var obj = JsonSerializer.Deserialize<Category>(json, opt);

        Console.WriteLine(json);
        Console.ReadKey();
    }

结果;

"{\"Names1\":[\"1\"],\"Name2\":\"2\",\"Name3\":\"3\"}"

我已经阅读了这篇文章,但我不确定它是否能帮助我对私有属性进行序列化/反序列化。在我看来,自定义JsonConverter允许我重写某个属性的序列化方式,但不能控制是否序列化(对于私有属性而言是如此)。如果我理解有误,请纠正我。 - Mats
3
@Mats - 你不需要为该属性编写转换器,而是要编写整个对象的转换器,并通过转换器手动序列化它(包括所有私有字段)。 - dbc
@Mats 我认为这篇文章可能会对你有所帮助,但我没有深入研究。我会更新我的答案,并提供一些序列化的代码片段,也许它可以指引你更进一步,因为除了你自己的方法外,没有其他途径(System.Text Json不提供此功能)。 - anilcemsimsek
@anilcemsimsek 哦,你说得对!我已经解决了。为了让它成为下一个遇到同样问题的人有用和完整的答案,我想请你提供一些反序列化示例。你知道为什么JSON输出中所有引号都被转义了吗?而不是 {"Names1": ...} 我认为它应该是 {"Names1":...}。我试着玩弄JavascriptEncoder,但没有成功。 - Mats
@Mats 好的,我更新了我的答案,并添加了反序列化的代码片段。请检查它是否适用于你的问题。 - anilcemsimsek

6
.NET 7及更高版本中,Microsoft添加了编程自定义System.Text.Json为每个.NET类型创建的序列化合同的能力。使用此API,您可以添加typeInfo modifier以序列化所选(或所有)选定类型的私有属性。
例如,您可能想要:
  1. 序列化所有标记有某些自定义属性的私有属性。

  2. 序列化特定类型的所有私有属性。

  3. 按名称序列化特定类型的特定私有属性。

鉴于这些要求,请定义以下属性和修饰符:
[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class JsonIncludePrivatePropertyAttribute : System.Attribute { }

public static partial class JsonExtensions
{
    public static Action<JsonTypeInfo> AddPrivateProperties<TAttribute>() where TAttribute : System.Attribute => typeInfo => 
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        foreach (var type in typeInfo.Type.BaseTypesAndSelf().TakeWhile(b => b != typeof(object)))
            AddPrivateProperties(typeInfo, type, p => Attribute.IsDefined(p, typeof(TAttribute)));
    };

    public static Action<JsonTypeInfo> AddPrivateProperties(Type declaredType) => typeInfo => 
        AddPrivateProperties(typeInfo, declaredType, p => true);
    
    public static Action<JsonTypeInfo> AddPrivateProperty(Type declaredType, string propertyName) => typeInfo => 
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object || !declaredType.IsAssignableFrom(typeInfo.Type))
            return;
        var propertyInfo = declaredType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic);
        if (propertyInfo == null)
            throw new ArgumentException(string.Format("Private roperty {0} not found in type {1}", propertyName, declaredType));
        if (typeInfo.Properties.Any(p => p.GetMemberInfo() == propertyInfo))
            return;
        AddProperty(typeInfo, propertyInfo);
    };

    static void AddPrivateProperties(JsonTypeInfo typeInfo, Type declaredType, Func<PropertyInfo, bool> filter)
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object || !declaredType.IsAssignableFrom(typeInfo.Type))
            return;
        var propertyInfos = declaredType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic);
        foreach (var propertyInfo in propertyInfos.Where(p => p.GetIndexParameters().Length == 0 && filter(p)))
            AddProperty(typeInfo, propertyInfo);
    }
    
    static void AddProperty(JsonTypeInfo typeInfo, PropertyInfo propertyInfo)
    {
        if (propertyInfo.GetIndexParameters().Length > 0)
            throw new ArgumentException("Indexed properties are not supported.");
        var ignore = propertyInfo.GetCustomAttribute<JsonIgnoreAttribute>();
        if (ignore?.Condition == JsonIgnoreCondition.Always)
            return;
        var name = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name 
            ?? typeInfo.Options?.PropertyNamingPolicy?.ConvertName(propertyInfo.Name) 
            ?? propertyInfo.Name;
        var property = typeInfo.CreateJsonPropertyInfo(propertyInfo.PropertyType, name);
        property.Get = CreateGetter(typeInfo.Type, propertyInfo.GetGetMethod(true));
        property.Set = CreateSetter(typeInfo.Type, propertyInfo.GetSetMethod(true));
        property.AttributeProvider = propertyInfo;
        property.CustomConverter = propertyInfo.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType is {} converterType
            ? (JsonConverter?)Activator.CreateInstance(converterType)
            : null;
        // TODO: handle ignore?.Condition == JsonIgnoreCondition.Never,  WhenWritingDefault, or WhenWritingNull by setting property.ShouldSerialize appropriately
        // TODO: handle JsonRequiredAttribute, JsonNumberHandlingAttribute
        typeInfo.Properties.Add(property);
    }

    delegate TValue RefFunc<TObject, TValue>(ref TObject arg);
    
    static Func<object, object?>? CreateGetter(Type type, MethodInfo? method)
    {
        if (method == null)
            return null;
        var myMethod = typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.CreateGetterGeneric), BindingFlags.NonPublic | BindingFlags.Static)!;
        return (Func<object, object?>)(myMethod.MakeGenericMethod(new[] { type, method.ReturnType }).Invoke(null, new[] { method })!);
    }

    static Func<object, object?> CreateGetterGeneric<TObject, TValue>(MethodInfo method)
    {
        if (method == null)
            throw new ArgumentNullException();
        if(typeof(TObject).IsValueType)
        {
            // https://dev59.com/rlLTa4cB1Zd3GeqPZVF9
            // https://dev59.com/Z0jSa4cB1Zd3GeqPHbDH#1212396
            var func = (RefFunc<TObject, TValue>)Delegate.CreateDelegate(typeof(RefFunc<TObject, TValue>), null, method);
            return (o) => {var tObj = (TObject)o; return func(ref tObj); };
        }
        else
        {
            var func = (Func<TObject, TValue>)Delegate.CreateDelegate(typeof(Func<TObject, TValue>), method);
            return (o) => func((TObject)o);
        }
    }

    static Action<object,object?>? CreateSetter(Type type, MethodInfo? method)
    {
        if (method == null)
            return null;
        var myMethod = typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.CreateSetterGeneric), BindingFlags.NonPublic | BindingFlags.Static)!;
        return (Action<object,object?>)(myMethod.MakeGenericMethod(new [] { type, method.GetParameters().Single().ParameterType }).Invoke(null, new[] { method })!);
    }
    
    static Action<object,object?>? CreateSetterGeneric<TObject, TValue>(MethodInfo method)
    {
        if (method == null)
            throw new ArgumentNullException();
        if (typeof(TObject).IsValueType)
        {
            // TODO: find a performant way to do this.  Possibilities:
            // Box<T> from Microsoft.Toolkit.HighPerformance
            // https://dev59.com/KWMk5IYBdhLWcg3w0hI-
            return (o, v) => method.Invoke(o, new [] { v });
        }
        else
        {
            var func = (Action<TObject, TValue?>)Delegate.CreateDelegate(typeof(Action<TObject, TValue?>), method);
            return (o, v) => func((TObject)o, (TValue?)v);
        }
    }

    static MemberInfo? GetMemberInfo(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo);
    
    static IEnumerable<Type> BaseTypesAndSelf(this Type? type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType;
        }
    }
}

那么,如果你的模型看起来像:

public partial class Model
{
    List<int> PrivateList { get; set; } = new();

    [JsonIgnore]  // For testing purposes only
    public List<int> SurrogateList { get => PrivateList; set => PrivateList = value; }
}

然后您可以使用[JsonIncludePrivateProperty]标记PrivateList:
public partial class Model
{
    [JsonIncludePrivateProperty]
    List<int> PrivateList { get; set; } = new();

并使用以下选项进行序列化:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { JsonExtensions.AddPrivateProperties<JsonIncludePrivatePropertyAttribute>() },
    },
};

或者如果您无法更改您的模型,您可以将其所有私有属性包含如下:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { JsonExtensions.AddPrivateProperties(typeof(Model)) },
    },
};

或者只返回名为PrivateList的属性,如下所示:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { JsonExtensions.AddPrivateProperty(typeof(Model), "PrivateList") },
    },
};

使用上述任何选项,生成的JSON将为例如{"PrivateList":[1,2,3]}

注意事项:

  • 不建议自动序列化所有类型的私有属性,但如果出于某种原因需要这样做,请使用以下修饰符:

    public static Action<JsonTypeInfo> AddPrivateProperties() => typeInfo => 
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        foreach (var type in typeInfo.Type.BaseTypesAndSelf().TakeWhile(b => b != typeof(object)))
            AddPrivateProperties(typeInfo, type, p => true);
    };
    
  • 截至 .NET 7,无法访问 System.Text.Json 的构造函数元数据,因此似乎没有办法将私有属性序列化并将其反序列化为构造函数参数。

  • 有一种类型信息修改器可以导致私有字段被序列化,请参见文档示例 自定义 JSON 合同:示例:序列化私有字段

  • 可能会在基类和派生类中具有相同名称的私有属性。如果尝试序列化两者的私有属性,则可能会出现异常

    System.InvalidOperationException: The JSON property name for 'Type.PropertyName' collides with another property.

    如果发生这种情况,您将把其中一个属性映射到另一个名称,例如通过向其中一个添加 [JsonPropertyName("SomeAlternateName")]

演示代码片段请点击这里


3

根据Microsoft 文档,System.Text.Json 在 .NET 5 开始部分地支持私有属性序列化。

通过 [JsonInclude] 属性,System.Text.Json 支持私有和内部属性的设置和获取。

请注意上述文档中非常具体的措辞。这意味着如果您有以下属性:

private string MyProperty { get; set; }

那么[JsonInclude]将不起作用。但是,如果您将此属性声明如下:

public string MyProperty { private get; private set; }

然后它将按预期工作。

这里查找更多详细信息。


8
对我来说不起作用:'...SubscriptionModel'类型上的'非公共属性'TenantsSource'被注释为无效的'JsonIncludeAttribute'。'。看起来,如果getter或setter中有一个是private,则[JsonInclude]有效,但如果两者都是private则无效。 - Felix
1
说实话,我并不喜欢在领域模型上使用这样的属性。对于这种私有模型序列化,我们仍然使用Newtonsoft json。 - Manzur Alahi
3
[JsonInclude] - "可以使用非公共的getter和setter... 不支持非公共属性。" - 文档 - Pang
3
@IanKemp - 在C#中,你不能同时为setter和getter指定修饰符,否则会出现编译错误“无法为属性或索引器'Model.MyProperty'的访问器指定可访问性修饰符”,请参见https://dotnetfiddle.net/hZEMFH。 - dbc

2

虽然您不能直接序列化私有字段,但可以间接地实现。

您需要为该字段提供一个公共属性和一个构造函数,如下面的示例:

class MyNumbers
{
    // This private field will not be serialized
    private List<int> _numbers;

    // This public property will be serialized
    public IEnumerable<int> Numbers => _numbers;

    // The serialized property will be recovered with this dedicated constructor
    // upon deserialization. Type and name must be the same as the public property.
    public MyNumbers(IEnumerable<int> Numbers = null)
    {
        _numbers = Numbers as List<int> ?? Numbers?.ToList() ?? new();
    }
}

以下代码演示了它是如何工作的:
string json;
// Serialization
{
    MyNumbers myNumbers = new(new List<int> { 10, 20, 30});
    json = JsonSerializer.Serialize(myNumbers);
    Console.WriteLine(json);
}
// Deserialization
{
    var myNumbers2 = JsonSerializer.Deserialize<MyNumbers>(json);
    foreach (var number in myNumbers2.Numbers)
        Console.Write(number + "  ");
}

输出:

{"Numbers":[10,20,30]}
10  20  30

如果您希望阻止他人访问您的私人数据,可以将名称更改为明确禁止的名称,例如__private_numbers

class MyNumbers2
{
    private List<int> _numbers;

    public IEnumerable<int> __private_numbers => _numbers;

    public MyNumbers2(IEnumerable<int> __private_numbers = null)
    {
        _numbers = __private_numbers as List<int> ?? __private_numbers?.ToList() ?? new();
    }
}

如果一个外部编码人员愚蠢到像它是该类的正常编程接口一样访问私有数据,那么他就应该受到谴责。您完全有权更改该“私有接口”,而无需感到任何内疚。并且他也不能通过 IEnumerable 搞乱您的内部列表。

在大多数情况下,这应该足够了。


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