如何在System.Text.Json自定义JsonConverter中解决复杂类型循环引用序列化问题
自定义JsonConverter循环引用问题解决方案
问题原因
该报错的核心原因是自定义JsonConverter的手写序列化逻辑完全绕开了System.Text.Json内置的引用追踪流程:ReferenceHandler.Preserve的引用校验、$id/$ref生成逻辑只会在内置的默认序列化流程中触发,你手动调用WriteStartObject、写入属性的操作不会触发这部分逻辑,因此即便配置了ReferenceHandler.Preserve也无法识别循环引用。
解决方案
方案1:使用内置属性过滤机制替代自定义Converter(最稳妥,兼容所有版本)
如果你的需求仅为固定忽略指定属性,完全不需要实现自定义Converter,直接给要忽略的属性添加[JsonIgnore]特性即可,这样所有内置的引用处理、序列化逻辑都能正常生效,不会出现循环引用问题:
public class User { public string Name { get; set; } [JsonIgnore] public int Age { get; set; } public User Reference { get; set; } }
序列化时仅需保留ReferenceHandler.Preserve配置,不需要注册自定义Converter,即可正常序列化带自引用的对象。
如果需要动态过滤属性,.NET 6及以上版本可以使用自定义JsonTypeInfo修饰器实现,同样不需要编写自定义Converter,可复用全部内置序列化能力。
方案2:自定义Converter内部手动维护引用追踪
如果必须使用自定义Converter实现特殊序列化逻辑,可以在Converter内部手动维护引用缓存,模拟内置的$id/$ref逻辑,适配ReferenceHandler.Preserve的输出格式:
public class UserJsonConverter : JsonConverter<User> { // 使用ThreadStatic避免多线程场景下引用缓存冲突 [ThreadStatic] private static Dictionary<User, string> _referenceCache; [ThreadStatic] private static int _referenceIdCounter; private void InitCacheIfNeeded() { if (_referenceCache == null) { _referenceCache = new Dictionary<User, string>(); _referenceIdCounter = 1; } } public override User Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotImplementedException(); } public override void Write(Utf8JsonWriter writer, User value, JsonSerializerOptions options) { InitCacheIfNeeded(); writer.WriteStartObject(); // 处理已有引用的对象,直接输出$ref if (_referenceCache.TryGetValue(value, out var existingRefId)) { writer.WriteString("$ref", existingRefId); writer.WriteEndObject(); return; } // 新对象生成唯一$id var newId = _referenceIdCounter.ToString(); _referenceCache.Add(value, newId); _referenceIdCounter++; writer.WriteString("$id", newId); // 写入自定义业务属性 writer.WriteString(nameof(value.Name), value.Name); writer.WritePropertyName(nameof(value.Reference)); JsonSerializer.Serialize(writer, value.Reference, options); writer.WriteEndObject(); // 根对象序列化完成后清空缓存,避免内存泄漏 if (_referenceCache.Count == 1) { _referenceCache.Clear(); _referenceIdCounter = 1; } } }
该实现完全兼容ReferenceHandler.Preserve的输出格式,可正确处理自引用、循环引用场景,不会再抛出循环引用异常。
内容的提问来源于stack exchange,提问作者Alexander Sheremetyev
相关产品推荐
相关产品推荐

