You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#使用System.Text.Json实现书签多态反序列化报错排查

使用System.Text.Json实现书签多态反序列化的问题与解决

问题描述

尝试不依赖Newtonsoft.Json,仅用System.Text.Json对书签JSON文件做多态反序列化时遇到两个问题:

  • 转换器类的Read方法处理folder类型时抛出System.NotSupportedException,已尝试给各类添加[JsonConstructor]属性但无效;
  • 省略Folder类中FolderElements列表的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);
        }
    }
}

示例书签JSON

{
   "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'.

解决方案

问题1:反序列化folder类型时抛出异常的解决

核心原因:转换器Read方法中反序列化Folder对象时,没有传入包含自定义转换器的JsonSerializerOptions。当反序列化Folder里的List<BookmarkElement>时,System.Text.Json默认无法处理抽象类的反序列化,导致报错。

修改步骤:

  1. 在反序列化入口处,创建JsonSerializerOptions并添加自定义转换器:
    var options = new JsonSerializerOptions();
    options.Converters.Add(new BookmarkElementConverter());
    var bookmarkModel = JsonSerializer.Deserialize<BookmarkModel>(jsonContent, options);
    
  2. 修改转换器的Read方法,反序列化时传入options参数:
    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 JsonSerializer.Deserialize<Bookmark>(jsonType, options);
                case "folder":
                    return JsonSerializer.Deserialize<Folder>(jsonType, options);
                default:
                    throw new JsonException();
            }
        }
    }
    
    这样反序列化Folder的children列表时,会自动使用自定义转换器处理BookmarkElement的多态类型。

问题2:省略getter/setter后缺失children属性的解决

核心原因:System.Text.Json默认只序列化可访问的属性(带getter/setter),如果省略getter/setter,字段变为私有,默认不会被序列化。

解决方法:

  • 保留FolderElements的getter和setter(推荐),确保属性可访问;
  • 如果必须使用字段,给字段添加[JsonPropertyName("children")]属性,显式指定序列化的JSON键名:
    [JsonPropertyName("children")]
    public List<BookmarkElement> FolderElements;
    

额外优化:合并Root与Folder类

Root类的结构和Folder完全一致,没必要单独定义,可以直接用Folder代替Root,简化模型:

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

内容的提问来源于stack exchange,提问作者Gnome-Improvement713

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 16:47:35