使用 JSON.NET 对对象进行动态属性名称序列化

15

为了连接REST API,我正在使用JSON.NET对对象进行序列化。我的对象中有一个需要序列化为JSON的属性具有动态属性名称。 如果结构中此属性的值为数字,则JSON属性为"type_id",但如果该值为字符串,则JSON属性名称为"type_code"。我尝试使用自定义JsonConverter来实现,但是当我尝试序列化时,会出现JsonWriterException并显示以下消息:

"Token PropertyName in state Property would result in an invalid JSON object. Path ''."

下面是我的对象的子集,如下所示,我没有为其指定对象中的属性名称:

[JsonProperty("title",Required=Required.Always,Order=1)]
public string Title { get; set; }

[JsonProperty("date",Order=3)]
[JsonConverter(typeof(IsoDateTimeConverter))]
public DateTime Date { get; set; }

[JsonProperty(Order=2)]
[JsonConverter(typeof(TypeIdentifierJsonConverter))]
public TypeIdentifier DocTypeIdentifier { get; set; }

在TypeIdentifier类中,我在WriteJson()方法中有以下内容:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    TypeIdentifier docTypeId;
    id= (TypeIdentifier) value;

    writer.WritePropertyName(id.ParameterName);
    writer.WriteValue(id.Value);           
}

然而,我假设它默认使用对象属性的名称而不是我的自定义名称,在JSON字符串中导致一个值有两个属性名称。如何动态设置属性名称?因为当未明确指定时,JsonPropertyAttribute 标签似乎会提取对象的属性名称。

注意: 这个对象永远不需要从这个应用程序进行反序列化。

编辑: 这个对象带有 [JsonObject(MemberSerialization.OptIn)] 属性


你的对象上有[Serializable]属性吗? - Robert
@Robert 使用 JSON.NET 而不是内置的 .NET 序列化程序,这意味着那些被忽略了。虽然如此,我确实为该类设置了 [JsonObject(MemberSerialization.OptIn)] 属性。 - JNYRanger
1个回答

23
一个JsonConverter不能为父对象中的属性设置名称。当调用转换器的WriteJson方法时,属性名称已经被写入JSON;写入器只期望一个指向值的值。这就是您遇到错误的原因。为了使此工作正常,必须为父对象制作自定义转换器。然后,该转换器将负责编写其子级的属性名称和值。
跟进: 可以编写一个针对父对象的转换器,以使仍尊重应用于其的JSON属性,同时仍然实现您想要的结果。我将在下面概述方法。
首先,进行一些设置。由于您没有说明您的类叫什么,因此我假设此示例称为Document。我们只需要对其进行一个实质性的更改,即从DocTypeIdentifier属性中删除[JsonConverter]属性。所以有:
[JsonObject(MemberSerialization.OptIn)]
class Document
{
    [JsonProperty("title", Required = Required.Always, Order = 1)]
    public string Title { get; set; }

    [JsonProperty("date", Order = 3)]
    [JsonConverter(typeof(IsoDateTimeConverter))]
    public DateTime Date { get; set; }

    [JsonProperty(Order = 2)]
    public TypeIdentifier DocTypeIdentifier { get; set; }

    public string OtherStuff { get; set; }
}

您还没有展示TypeIdentifier类的代码,因此我会假设它看起来像这样,举个例子:

class TypeIdentifier
{
    public string Value { get; set; }
    public string ParameterName { get; set; }
}

有了这个前置条件,我们可以开始制作转换器。这种方法非常简单:我们将 Document 加载到 JObject 中,利用它遵循应用的属性的事实,然后返回并修复 DocTypeIdentifier 的序列化,因为它需要特殊处理。一旦我们完成了这个步骤,我们就将 JObject 写入 JsonWriter 中。以下是代码:

class DocumentConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Document));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Document doc = (Document)value;

        // Create a JObject from the document, respecting existing JSON attribs
        JObject jo = JObject.FromObject(value);

        // At this point the DocTypeIdentifier is not serialized correctly.
        // Fix it by replacing the property with the correct name and value.
        JProperty prop = jo.Children<JProperty>()
                           .Where(p => p.Name == "DocTypeIdentifier")
                           .First();

        prop.AddAfterSelf(new JProperty(doc.DocTypeIdentifier.ParameterName, 
                                        doc.DocTypeIdentifier.Value));
        prop.Remove();

        // Write out the JSON
        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

现在我们有了转换器,但问题是我们不能简单地在Document类上使用[JsonConverter]属性来使用它。如果这样做,当我们将文档加载到JObject中时,转换器会尝试使用自身,导致递归循环。因此,我们需要创建转换器的实例,并通过设置将实例传递给序列化程序。转换器的CanConvert方法确保它在正确的类上使用。JObject.FromObject方法在内部使用不同的序列化程序实例,因此它看不到DocumentConverter,也就不会遇到麻烦。

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new DocumentConverter());

string json = JsonConvert.SerializeObject(doc, settings);

这里是一个演示,展示了转换器的运作:

class Program
{
    static void Main(string[] args)
    {
        Document doc = new Document
        {
            Title = "How to write a JSON converter",
            Date = DateTime.Today,
            DocTypeIdentifier = new TypeIdentifier
            {
                ParameterName = "type_id",
                Value = "26"
            },
            OtherStuff = "this should not appear in the JSON"
        };

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.Converters.Add(new DocumentConverter());
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(doc, settings);
        Console.WriteLine(json);
    }
}

这是上述代码的输出结果:

{
  "title": "How to write a JSON converter",
  "type_id": "26",
  "date": "2014-03-28T00:00:00-05:00"
}

所以本质上我需要编写一个转换器来处理整个对象,使用属性进行自动序列化在这里行不通?既然我已经为内部对象编写了几个转换器,那么当我到达那些字段时,我能否从父转换器中调用它们,以便我无需重新编写它们? - JNYRanger
1
有一种方法可以编写转换器,使属性的属性仍然得到尊重(这意味着其他转换器应该自动调用),但问题在于,您不能在父对象上放置[JsonConverter]属性,否则它将导致递归循环,因为转换器试图使用自身。给我几分钟时间,我会尝试制作一个示例来演示。 - Brian Rogers
非常感谢。一个例子会很有帮助,我一直在做绕过的研究,扩展DefaultContractResolver似乎并不适合我的需求,而且我也不想首先使用那么多反射。 - JNYRanger
没问题!我希望我能再次为你点赞,非常感谢你提供这么详细的解释。它终于解决了我的问题,而不需要笨拙的解决方法! - JNYRanger

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