书签文件的 C# 多态反序列化

C# Polymorphic Deserialization of Bookmarks file

提问人:Gnome-Improvement713 提问时间:8/20/2023 最后编辑:dbcGnome-Improvement713 更新时间:8/20/2023 访问量:60

问:

我正在尝试反序列化书签文件。具体来说,使用没有 Newtonsoft 的多态反序列化。运行时,我在转换器类的 Read 方法中出现异常,返回大小写“文件夹”。看起来我需要某种文件夹或其基类的构造函数。我尝试在每个类中使用[JsonConstructor]属性,但没有运气。

此外,当我省略 Folder 的 FolderElement 列表的 getter 和 setter 时,程序会编译并运行,但在 JSON 输出中,仅创建“folder”类型的对象,并且它们缺少“children”属性。

public abstract class BookmarkElement
{
    public BookmarkElement() {}
    [JsonPropertyName("date_added")]
    public string DateAdded { get; set; }
    [JsonPropertyName("date_last_used")]
    public string DateLastUsed { get; set; }
    [JsonPropertyName("guid")]
    public string Guid { get; set; }
    [JsonPropertyName("id")]
    public string Id { get; set; }
    [JsonPropertyName("name")]
    public string Name { get; set; }
    [JsonPropertyName("type")]
    public string Type { get; set; }
}

public class Bookmark : BookmarkElement
{
    public Bookmark() { }
    [JsonPropertyName("url")]
    public string Url {get; set;}
}

public class Folder : BookmarkElement
{
    public Folder() {}
    [JsonPropertyName("date_modified")]
    public string DateModified { get; set; }
    [JsonPropertyName("children")]
    public List<BookmarkElement> FolderElements {get; set;}
}

public class Root : BookmarkElement
{
    public Root() {}
    [JsonPropertyName("date_modified")]
    public string DateModified { get; set; }
    [JsonPropertyName("children")]
    public List<BookmarkElement> RootFolder { get; set; }
}

public class BookmarkModel
{
    public BookmarkModel() {}
    [JsonPropertyName("checksum")]
    public string Checksum { get; set; }
    [JsonPropertyName("roots")]
    public Dictionary<string, Root> Roots { get; set; }
    [JsonPropertyName("version")]
    public int Version { get; set; }
}

JSON转换器

public class BookmarkElementConverter : JsonConverter<BookmarkElement>
{
    public override bool CanConvert(Type typeToConvert) => 
        typeof(BookmarkElement).IsAssignableFrom(typeToConvert);

    public override BookmarkElement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if(reader.TokenType != JsonTokenType.StartObject) throw new JsonException();

        using (var jsonDocument = JsonDocument.ParseValue(ref reader))
        {
            if(!jsonDocument.RootElement.TryGetProperty("type", out var typeProperty)) throw new JsonException();

            var jsonType = jsonDocument.RootElement.GetRawText();

            switch(typeProperty.GetString())
            {
                case "url":
                    return (Bookmark)JsonSerializer.Deserialize(jsonType, typeof(Bookmark));
                case "folder":
                    return (Folder)JsonSerializer.Deserialize(jsonType, typeof(Folder));
                default:
                    throw new JsonException();
            }
        }
    }

    public override void Write(Utf8JsonWriter writer, BookmarkElement value, JsonSerializerOptions options)
    {
        if (value is Bookmark bookmark)
        {
            JsonSerializer.Serialize(writer, bookmark);
        }
        else if (value is Folder folder)
        {
            JsonSerializer.Serialize(writer, folder);
        }
    }
}

示例书签文件

{
   "checksum": "cc1f5c62ec7814f7928e2befab26c311",
   "roots": {
      "bookmark_bar": {
         "children": [ {
            "children": [  ],
            "date_added": "13335767383821356",
            "date_last_used": "0",
            "date_modified": "13335767383821356",
            "guid": "efeb5549-612d-4656-8982-a17069075213",
            "id": "13",
            "name": "test",
            "type": "folder"
         }, {
            "date_added": "13335767548044529",
            "date_last_used": "0",
            "guid": "df7a482b-c1c5-4aa2-af8e-8cee539513d9",
            "id": "15",
            "name": "DuckDuckGo — Privacy, simplified.",
            "type": "url",
            "url": "https://duckduckgo.com/"
         } ],
         "date_added": "13335764354355200",
         "date_last_used": "0",
         "date_modified": "13335767548044529",
         "guid": "0bc5d13f-2cba-5d74-951f-3f233fe6c908",
         "id": "1",
         "name": "Bookmarks bar",
         "type": "folder"
      },
      "other": {
         "children": [  ],
         "date_added": "13335764354355201",
         "date_last_used": "0",
         "date_modified": "0",
         "guid": "82b081ec-3dd3-529c-8475-ab6c344590dd",
         "id": "2",
         "name": "Other bookmarks",
         "type": "folder"
      },
      "synced": {
         "children": [  ],
         "date_added": "13335764354355202",
         "date_last_used": "0",
         "date_modified": "0",
         "guid": "4cf2e351-0e85-532b-bb37-df045d8f8d0f",
         "id": "3",
         "name": "Mobile bookmarks",
         "type": "folder"
      }
   },
   "version": 1
}

例外

Exception has occurred: CLR/System.NotSupportedException
An exception of type 'System.NotSupportedException' occurred in System.Text.Json.dll but was not handled in user code: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'BookmarkReader.Model.BookmarkElement'. Path: $.children[0] | LineNumber: 1 | BytePositionInLine: 24.'
 Inner exceptions found, see $exception in variables window for more details.
 Innermost exception     System.NotSupportedException : Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'BookmarkReader.Model.BookmarkElement'.
C# .net-core system.text.json polymorphic-deserialization

评论

0赞 dbc 8/21/2023
不知道,很多问题无缘无故地被否决了。问题的 JSON 示例有点复杂,但必须如此,因为只有当对象嵌套并且转换器需要递归调用自身时,问题才会浮出水面。为了改善这个问题,您可能做的唯一一件事就是显示您和您的电话。事实上,您只有 98% 的最小可重现示例BookmarkElementJsonSerializerOptionsJsonSerializer.Serialize()

答:

0赞 dbc 8/20/2023 #1

导致该错误的原因是,在某些时候,您的转换器没有被拾取,并且您的代码正在尝试反序列化 类型的对象,该对象是抽象的,因此无法构造。Microsoft 的错误消息文本具有误导性,因此请参阅演示小提琴 #1,它完全省略了转换器以进行确认。BookmarkElement

要解决此问题,请进行修复,以便将传入的选项(将包含在转换器列表中)传递到 。您需要这样做,因为您的数据模型是递归的,因此转换器也必须以递归方式调用。您还必须修复为仅对抽象类型 BookmarkElement 返回 true。这是默认行为,因此您可以删除替代。对于派生的具体类型,转换器不是必需的,如果尝试将转换器用于派生类型,则会收到堆栈溢出异常。BookmarkElementConverter().Read()BookmarkElementConverterJsonSerializer.Deserialize()BookmarkElementCanConvert()

因此,应如下所示,并进行了一些代码简化:BookmarkElementConverter

public class BookmarkElementConverter : JsonConverter<BookmarkElement>
{
    public override BookmarkElement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if(reader.TokenType != JsonTokenType.StartObject) 
            throw new JsonException();

        using (var jsonDocument = JsonDocument.ParseValue(ref reader))
        {
            if(!jsonDocument.RootElement.TryGetProperty("type", out var typeProperty)) 
                throw new JsonException();
            return typeProperty.GetString() switch
            {
                "url" => jsonDocument.RootElement.Deserialize<Bookmark>(options),
                "folder" => jsonDocument.RootElement.Deserialize<Folder>(options),
                _ => throw new JsonException(),
            };
        }
    }

    public override void Write(Utf8JsonWriter writer, BookmarkElement value, JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
}

然后,当您反序列化时,请务必包含在 :BookmarkElementConverterJsonSerializerOptions.Converters

var options = new JsonSerializerOptions
{
    Converters = { new BookmarkElementConverter() },
    // Add any additional required options here:
    WriteIndented = true,
};

var model = JsonSerializer.Deserialize<BookmarkModel>(json, options);

在这里演示小提琴 #2。

评论

0赞 Gnome-Improvement713 8/21/2023
谢谢你的详细回答。我需要一些时间来解开所有这些。标记为公认的答案,尽管我无法让它像在演示站点中那样在本地正确运行,但我确定这是我的错。谢谢!
0赞 dbc 8/21/2023
@NotIntelligentException - 您使用的是哪个 .NET 版本?我针对 .NET 7 进行了测试。
0赞 Gnome-Improvement713 8/22/2023
我在本地使用 .NET 7。自从我上次发表评论以来,没有更改要排除故障。
0赞 dbc 8/22/2023
@NotIntelligentException - 然后,如果您仍然遇到问题,请编辑您的原始问题,以包含一个最小的可重现示例,准确显示事情是如何失败的。
0赞 dbc 8/22/2023
@NotIntelligentException - 顺便说一句,我建议将抽象的获取方式(即 然后在派生类中重写它以返回所需的类型标识符。或者,可以切换到使用 .NET 7 的内置多态性机制,请参阅 System.Text.Json 中是否可能进行多态反序列化?Typepublic abstract string Type { get; }