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默认无法处理抽象类的反序列化,导致报错。
修改步骤:
- 在反序列化入口处,创建
JsonSerializerOptions并添加自定义转换器:var options = new JsonSerializerOptions(); options.Converters.Add(new BookmarkElementConverter()); var bookmarkModel = JsonSerializer.Deserialize<BookmarkModel>(jsonContent, options); - 修改转换器的
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
相关产品推荐
相关产品推荐

