将JSON反序列化为多个属性

15
我正在针对一个第三方API进行编程,该API返回JSON数据,但格式可能有些奇怪。某些属性既可以是一个对象(包含一个Id属性),也可以是一个字符串(表示对象的Id)。例如,以下两种格式都是有效的:
{
    ChildObject: 'childobjectkey1'
}

并且

{
    ChildObject: {
        Id: 'childobjectkey1',
        // (other properties)
    }
}

我正在尝试使用JSON.net将其反序列化为强类型类,但是到目前为止运气不太好。我最好的想法是将其序列化为两个属性,一个是字符串,另一个是对象,并使用自定义的JsonConverter来允许变量行为。
public abstract class BaseEntity
{
    public string Id { get; set; }
}

public class ChildObject : BaseEntity { }

public class MyObject
{
    [JsonProperty("ChildObject")]
    [JsonConverter(typeof(MyCustomIdConverter))]
    public string ChildObjectId { get; set; }

    [JsonProperty("ChildObject")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject { get; set; }
}

然而,对具有相同PropertyName的两个属性设置JsonProperty属性会导致异常:
Newtonsoft.Json.JsonSerializationException:在“.....”上已存在名称为'ChildObject'的成员。 请使用JsonPropertyAttribute指定另一个名称。
我相当确定如果能克服这个障碍,JsonConverter方法会起作用 - 我怀疑错误出在这里,因为JsonProperty属性既用于序列化又用于反序列化。在这种情况下,我对序列化这个类没有兴趣 - 它只会被用作反序列化的目标。
我对远程端没有控制权(它是第三方API),但我希望能够实现这种反序列化。我不介意使用我已经开始的方法,或者我还没有考虑过的方法。 这个问题也相关,但没有答案。

1
我认为转换器应该针对整个对象而不是单个字段...(顺便说一句,这种格式对于实体复杂的图形来说似乎相当合理) - Alexei Levenkov
希望避免为整个对象使用自定义转换器,因为有多个实例,并且某些类型具有多个类似的属性。 - Richard
3个回答

8
尝试以下代码(如果您将在代码中使用它,请进行充分验证):
public class MyObject
{
    public ChildObject MyChildObject;
    public string MyChildObjectId;

    [JsonProperty("ChildObject")]
    public object ChildObject
    {
        get
        {
            return MyChildObject;
        }
        set
        {
            if (value is JObject)
            {
                MyChildObject = ((JToken)value).ToObject<ChildObject>();
                MyChildObjectId = MyChildObject.Id;
            }
            else
            {
                MyChildObjectId = value.ToString();
                MyChildObject = null;
            }
        }
    }
}

有趣的想法;唯一的问题在于ChildObject属性不再是强类型,因此您需要在每次使用时适当地进行转换。 - Brian Rogers
1
有趣,我没有考虑过直接接收JObject。虽然我通常不想将逻辑拆分为一些在DTO上,但从我的程序集外部来看,这是最整洁的解决方案,因为对象类型的属性也可以设置为内部。谢谢! - Richard

6

不要为每个字段创建两个单独的转换器,最好为“主”属性创建一个单一的转换器,并将另一个连接到它。 ChildObjectId 是从 ChildObject 派生的。

public class MyObject
{
    [JsonIgnore]
    public string ChildObjectId
    {
        get { return ChildObject.Id; }

        // I would advise against having a setter here
        // you should only allow changes through the object only
        set { ChildObject.Id = value; }
    }

    [JsonConverter(typeof(MyObjectChildObjectConverter))]
    public ChildObject ChildObject { get; set; }
}

现在要转换ChildObject可能有些棘手。该对象有两种可能的表示方法:字符串或对象。确定您拥有的表示形式并执行转换。
public class MyObjectChildObjectConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ChildObject);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = serializer.Deserialize<JToken>(reader);
        switch (obj.Type)
        {
        case JTokenType.Object:
            return ReadAsObject(obj as JObject);
        case JTokenType.String:
            return ReadAsString((string)(JValue)obj);
        default:
            throw new JsonSerializationException("Unexpected token type");
        }
    }

    private object ReadAsObject(JObject obj)
    {
        return obj.ToObject<ChildObject>();
    }

    private object ReadAsString(string str)
    {
        // do a lookup for the actual object or whatever here
        return new ChildObject
        {
            Id = str,
        };
    }
}

3

以下是我在这种情况下会采取的方法。

  • 在父类中只有一个子对象属性,并将其设置为ChildObject类型
  • 创建一个自定义的JsonConverter,可以检查JSON并执行以下操作之一:
    • 如果数据存在,则反序列化完整的子对象实例;或者
    • 创建一个新的子对象实例并设置其ID,保留所有其他属性为空白。(或者您可以像Jeff Mercado建议的那样,根据ID从数据库加载对象,如果适用于您的情况。)
  • 可选地,在子对象上放置一个指示其是否完全填充的属性。转换器可以在反序列化过程中设置此属性。

反序列化后,如果JSON中存在ChildObject属性(带有ID或完整对象值),则保证拥有ChildObject实例,并且可以从中获取其ID;否则,如果JSON中没有ChildObject属性,则父类中的ChildObject属性将为空。

以下是一个完整的工作示例以进行演示。在此示例中,我修改了父类以包含三个ChildObject的不同JSON可能性(仅字符串ID、完整对象和两者都不存在)。它们都使用相同的转换器。我还向ChildObject类添加了一个Name属性和一个IsFullyPopulated属性。

以下是DTO类:

public abstract class BaseEntity
{
    public string Id { get; set; }
}

public class ChildObject : BaseEntity 
{
    public string Name { get; set; }
    public bool IsFullyPopulated { get; set; }
}

public class MyObject
{
    [JsonProperty("ChildObject1")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject1 { get; set; }

    [JsonProperty("ChildObject2")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject2 { get; set; }

    [JsonProperty("ChildObject3")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject3 { get; set; }
}

这里是转换器:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        ChildObject child = null;
        if (token.Type == JTokenType.String)
        {
            child = new ChildObject();
            child.Id = token.ToString();
            child.IsFullyPopulated = false;
        }
        else if (token.Type == JTokenType.Object)
        {
            child = token.ToObject<ChildObject>();
            child.IsFullyPopulated = true;
        }
        else if (token.Type != JTokenType.Null)
        {
            throw new JsonSerializationException("Unexpected token: " + token.Type);
        }
        return child;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

以下是演示转换器操作的测试程序:
class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""ChildObject1"": 
            {
                ""Id"": ""key1"",
                ""Name"": ""Foo Bar Baz""
            },
            ""ChildObject2"": ""key2""
        }";

        MyObject obj = JsonConvert.DeserializeObject<MyObject>(json);

        DumpChildObject("ChildObject1", obj.ChildObject1);
        DumpChildObject("ChildObject2", obj.ChildObject2);
        DumpChildObject("ChildObject3", obj.ChildObject3);
    }

    static void DumpChildObject(string prop, ChildObject obj)
    {
        Console.WriteLine(prop);
        if (obj != null)
        {
            Console.WriteLine("   Id: " + obj.Id);
            Console.WriteLine("   Name: " + obj.Name);
            Console.WriteLine("   IsFullyPopulated: " + obj.IsFullyPopulated);
        }
        else
        {
            Console.WriteLine("   (null)");
        }
        Console.WriteLine();
    }
}

以下是上述操作的输出结果:
ChildObject1
   Id: key1
   Name: Foo Bar Baz
   IsFullyPopulated: True

ChildObject2
   Id: key2
   Name:
   IsFullyPopulated: False

ChildObject3
   (null)

我自己也一直在这个方向上努力,但是尝试避免使用“IsFullyLoaded”属性的想法。我选择Alex的解决方案,因为我可以将所有混乱的部分隐藏在一个程序集中,并使其从外部看起来符合我的要求。 - Richard
这个解决方案并不依赖于存在IsFullyPopulated属性;如果您不需要它,可以直接将其删除。我添加它只是为了展示它的可行性,因为它可以轻松地告诉我们子对象是否拥有所有数据或仅有一个ID。但是,在父对象上有两个属性同样适用于此。最终,如何让您的代码工作取决于您自己的决定,所以如果另一个解决方案对您更好,我完全可以接受!干杯。 - Brian Rogers

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