【问题标题】:Is it possible to extend the JsonProperty attribute class with properties to affect different serializations?是否可以使用属性扩展 JsonProperty 属性类以影响不同的序列化?
【发布时间】:2021-11-27 00:29:04
【问题描述】:

我的问题与此类似:How to ignore JsonProperty(PropertyName = "someName") when serializing json? 其中的解决方案对我有用,但我很想知道是否可以使用更多属性扩展[JsonProperty] 属性(使用 Newtonsoft.json)?

一些背景:
我有一个应用程序(我们称之为 SmartModel),它根据用户输入在 c# 中生成物理模型。 SmartModel 由许多具有许多属性的类组成(例如,Pipe 类具有诸如LengthDiameter 等属性)。 SmartModel 写出一个 json 类型的 DTO,用于在单独的应用程序中进行求解(我们称之为 Solver 和 DTO,SolverDTO)。但是,除了这个 SmartModel 还写了一个不同的 DTO 用于保存和打开目的(方便地称为 SmartModelDTO)。

在这方面,在SmartModel中的某些属性之上有一个装饰器(例如[JsonProperty(SolverPropertyName = "someName")])会很方便,然后设置一个合约解析器来序列化和写出(以json格式):

  1. 生成 SolverDTO 时的SolverPropertyName
  2. 生成 SmartModelDTO 时的UnderlyingName

(其中UnderlyingName默认已经是JsonProperty的属性,SolverPropertyNameJsonProperty应该扩展的属性)。


编辑

这里有一个最小的代码示例来解释我想要实现的目标:

我有一个名为Pipe 的示例类,如下所示:

class Pipe
{
    [JsonProperty(PropertyName = "z_axis_dimension")]
    public double Length { get; set; }

    [JsonProperty(PropertyName = "cross_sectional_dimension")]
    public double Diameter { get; set; }
}

我想以两种不同的方式进行序列化(将来可能会更多)。对于 SmartModelDTO,我希望序列化程序使用 UnderlyingProperty,但对于 SolverDTO,我希望在 JsonProperty 属性中使用 PropertyName。为此,可以实现以下合约解析器:

class IgnoreJsonPropertyNameContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(
        Type type, 
        MemberSerialization memberSerialization)
    {
        IList<JsonProperty> list = base.CreateProperties(
            type, 
            memberSerialization);

        foreach (JsonProperty prop in list)
        {
            prop.PropertyName = prop.UnderlyingName;
        }

        return list;
    }
}

使用示例如下:

// Instance of Pipe:
Pipe pipe = new()
{
    Length = 10.2,
    Diameter = 5.5,
};

// Set some Json serializer settings:
JsonSerializerSettings jsonSerializerSettings = new();
jsonSerializerSettings.Formatting = Formatting.Indented;

// Serialize Pipe where property values are obtained from the
// JsonProperty PropertyName:
string solverJsonString = JsonConvert.SerializeObject(
    pipe, jsonSerializerSettings);
Console.WriteLine($"Serialized string for SolverDTO:");
Console.WriteLine($"{solverJsonString}");
Console.WriteLine();

// Set a new contract resolver to return the
// JsonProperty UnderlyingName instead of the PropertyName:
jsonSerializerSettings.ContractResolver =
    new IgnoreJsonPropertyNameContractResolver();

// Serialize Pipe where property values are obtained from the
// JsonProperty UnderlyingName:
string smartModelJsonString = JsonConvert.SerializeObject(
    pipe, jsonSerializerSettings);
Console.WriteLine($"Serialized string for SmartModelDTO:");
Console.WriteLine($"{smartModelJsonString}");

Console.ReadLine();

给出以下输出:

Serialized string for SolverDTO:
{
  "z_axis_dimension": 10.2,
  "cross_sectional_dimension": 5.5
}

Serialized string for SmartModelDTO:
{
  "Length": 10.2,
  "Diameter": 5.5
}

但是,我想在Pipe 中有一个功能来标记属性,例如如下:

class Pipe
{
    [JsonProperty(
    SolverPropertyName = "z_axis_dimension", 
    APIPropertyName = "distance")]
    public double Length { get; set; }

    [JsonProperty(
    SolverPropertyName = "cross_sectional_dimension", 
    APIPropertyName = "diameter")]
    public double Diameter { get; set; }
}

然后设置与上面类似的不同的合约解析器来序列化Pipe对象,但一个使用SolverPropertyName序列化,另一个用于APIPropertyName,等等......

[JsonProperty] 类可以扩展吗?除了PropertyName之外还有SolverPropertyNameAPIPropertyName等属性?

【问题讨论】:

  • 除了描述您的代码,您能给我们提供具有预期输入/输出的实际代码吗?最好尽可能小,即minimal reproducible example
  • @DavidG。肯定的事。我用文字解释它,因为我上面提到的帖子也以类似的方式解释它并得到了一些答案。然而,我现在的位置已经很晚了,我会在天亮时发布一个最小的样本。问候。
  • @DavidG。我在上面添加了一个最小的可重现示例。

标签: c# json serialization properties attributes


【解决方案1】:

我会将其添加为单独的答案。我认为我提供的其他答案仍然对您有用,并导致更简洁和更易于阅读的代码。

下面的这个方法可行,但它(恕我直言)更混乱(它可能会被改进)并且可能导致更多错误。

Newtonsoft 的 JsonProperty 属性是密封的,所以我们不能覆盖它。这意味着我们首先需要创建自己的属性。 在这里,我们定义了两个属性,它们对应于您要使用的名称。

[AttributeUsage(AttributeTargets.Property)]
public class MyJsonPropertyAttribute : Attribute 
{
    public string SolverPropertyName { get; set; }
    public string APIPropertyName { get; set; }
}

然后你可以这样装饰你的班级

public class Pipe
{
    [MyJsonProperty(
    SolverPropertyName = "z_axis_dimension",
    APIPropertyName = "distance")]
    public double Length { get; set; }

    [MyJsonProperty(
    SolverPropertyName = "cross_sectional_dimension",
    APIPropertyName = "diameter")]
    public double Diameter { get; set; }
}

接下来我们需要定义我们的自定义合约解析器,我们称之为DualNameContractResolver。在构造过程中,我们需要指定要使用的属性属性的名称。您也可以将其更改为枚举(这将减少错误)。 如果您指定了错误的名称,则将应用 Newtonsoft 的默认序列化。

public class DualNameContractResolver : DefaultContractResolver
{
    private readonly string _propertyDecoratorToUse;

    public DualNameContractResolver(string propertyDecoratorToUse)
    {
        _propertyDecoratorToUse = propertyDecoratorToUse ?? throw new ArgumentNullException(nameof(propertyDecoratorToUse));
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        CustomAttributeNamedArgument namedArgument = member.CustomAttributes.SelectMany(c => c.NamedArguments).Where(w => w.MemberName.Equals(_propertyDecoratorToUse)).FirstOrDefault();
        if(namedArgument.TypedValue.Value != null)
            property.PropertyName = namedArgument.TypedValue.Value.ToString();

        return property;
    }
}

如您所见,在 CreateProperty 方法中,我们查找请求的属性,如果找到,则应用定义的值(即序列化名称)。

然后你就可以像这样使用所有这些了。

Pipe pipe = new()
{
    Length = 10.2,
    Diameter = 5.5,
};

var solverResolver = new DualNameContractResolver("SolverPropertyName");                
var apiResolver = new DualNameContractResolver("APIPropertyName");    
JsonSerializerSettings jsonSerializerSettings = new();
jsonSerializerSettings.Formatting = Formatting.Indented;

jsonSerializerSettings.ContractResolver = solverResolver;
string json1 = JsonConvert.SerializeObject(pipe, jsonSerializerSettings);

jsonSerializerSettings.ContractResolver = apiResolver;
string json2 = JsonConvert.SerializeObject(pipe, jsonSerializerSettings);

【讨论】:

  • 这个答案非常适合我。我主要想要一个对象的所有属性,只是为不同的序列化使用不同的名称。因此,下面的解决方案可能会导致过多的代码库,只是为了在序列化发生时使用不同的属性名称。我知道属性的装饰器会使代码有点混乱。然而,就我而言,我有许多物理组件(如泵、管道、热交换器等......)。其中一些有 40 多个属性来完成定义。 “复制”(和映射)它们中的每一个都是过度的。
【解决方案2】:

由于缺少代码和示例(在撰写本文时),我不确定我是否完全想要您想要的。但这将有助于从相同的基础生成不同的序列化输出。

定义一个包含所有属性的接口

public interface IBase
{
    string PropertyOne { get; set; }
    string PropertyTwo { get; set; }
    string PropertyThree { get; set; }
    string PropertyFour { get; set; }
}

然后你的第一个类被序列化,然后使用我们定义的接口。 这里我们使用JsonProperty属性来定义我们想要的输出名称,与属性的实际名称不同。

我们还使用 NewtonSoft 的 ShouldSerialize 功能来抑制输出我们不想要的第四个属性。

public class First: IBase
{
    [JsonProperty("PropertyNumber1")]
    public string PropertyOne { get; set; }

    [JsonProperty("PropertyNumber2")]
    public string PropertyTwo { get; set; }

    [JsonProperty("PropertyNumber3")]
    public string PropertyThree { get; set; }

    public string PropertyFour { get; set; }
    public bool ShouldSerializePropertyFour() { return false; }
}

然后我们类似地定义我们的另一个类,但这里我们取消第一个属性,并通过JsonProperty给其余的一个不同的名称

public class Second : IBase
{
    [JsonProperty("SecondProperty")]
    public string PropertyTwo { get; set; }

    [JsonProperty("ThirdProperty")]
    public string PropertyThree { get; set; }

    [JsonProperty("FourthProperty")]
    public string PropertyFour { get; set; }

    public string PropertyOne { get; set; }
    public bool ShouldSerializePropertyOne() { return false; }
}

然后当我们序列化类时,我们只会得到我们想要的属性,并且它们有不同的名称。

除了使用接口,你也可以对抽象类做同样的事情,但是这里你需要在抽象类中将属性定义为virtual,并在子类中覆盖它们。请注意,为了让ShouldSerialize 工作,您需要先覆盖它,然后再取消它!

【讨论】:

  • @json.kaisersmith。谢谢回复。这并不完全是我想要的,但我在原始帖子中添加了一个最小的可重现样本,这可能会消除任何误解。
  • 感谢您的解决方案。虽然不完全是我在上面的问题中寻找的内容(鉴于我在评论中提供的对我选择实施的答案的上下文),但我的代码中有一个区域可以实现它。从我的模型中生成某些 json 时,我有时需要忽略某些属性,这会很方便。问候
猜你喜欢
  • 1970-01-01
  • 2019-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多