增强 ExtendableConverter 对属性忽略条件的支持 支持 DefaultIgnoreCondition 及属性级 JsonIgnore(Condition),序列化时可跳过默认值或空值属性,扩展字段也遵循全局忽略条件。新增 IsDefaultValue 方法,完善单元测试覆盖,优化 JsonDocument 释放。智能大石头 authored at 2026-04-07 21:11:42
diff --git a/NewLife.Core/Serialization/ExtendableConverter.cs b/NewLife.Core/Serialization/ExtendableConverter.cs
index acf36ae..affe131 100644
--- a/NewLife.Core/Serialization/ExtendableConverter.cs
+++ b/NewLife.Core/Serialization/ExtendableConverter.cs
@@ -12,6 +12,8 @@ namespace NewLife.Serialization;
/// 序列化时先输出类型的公共属性,再将 <see cref="IExtend.Items"/> 中的扩展字段追加到同一 JSON 对象层级。
/// 反序列化时按属性名匹配 JSON 字段并赋值给对应成员,无法匹配的多余字段写入 <see cref="IExtend.Items"/> 字典。
/// 属性名匹配遵循 <see cref="JsonSerializerOptions.PropertyNamingPolicy"/> 与 <see cref="JsonSerializerOptions.PropertyNameCaseInsensitive"/> 配置。
+/// 序列化时遵循 <see cref="JsonSerializerOptions.DefaultIgnoreCondition"/> 及属性级 <see cref="JsonIgnoreAttribute"/> 条件,
+/// 正确跳过默认值或空值属性。
/// </remarks>
public class ExtendableConverter : JsonConverter<Object>
{
@@ -30,7 +32,8 @@ public class ExtendableConverter : JsonConverter<Object>
if (!typeof(IExtend).IsAssignableFrom(typeToConvert))
return JsonSerializer.Deserialize(ref reader, typeToConvert, options)!;
- var jsonRoot = JsonDocument.ParseValue(ref reader).RootElement;
+ using var jsonDoc = JsonDocument.ParseValue(ref reader);
+ var jsonRoot = jsonDoc.RootElement;
var obj = Activator.CreateInstance(typeToConvert);
if (obj is IExtend extendable)
{
@@ -61,21 +64,53 @@ public class ExtendableConverter : JsonConverter<Object>
writer.WriteStartObject();
+ var ignoreCondition = options.DefaultIgnoreCondition;
var propMap = SerialHelper.GetJsonPropertyMap(value.GetType(), options);
foreach (var (jsonName, prop) in propMap)
{
if (!prop.CanRead) continue;
+
+ var propValue = prop.GetValue(value);
+
+ // 属性级 [JsonIgnore(Condition=...)] 优先于全局 DefaultIgnoreCondition
+ var attrCondition = prop.GetCustomAttribute<JsonIgnoreAttribute>()?.Condition;
+ var condition = attrCondition is JsonIgnoreCondition.WhenWritingNull or JsonIgnoreCondition.WhenWritingDefault
+ ? attrCondition.Value
+ : ignoreCondition;
+
+ if (condition == JsonIgnoreCondition.WhenWritingNull && propValue is null) continue;
+ if (condition == JsonIgnoreCondition.WhenWritingDefault && IsDefaultValue(propValue, prop.PropertyType)) continue;
+
writer.WritePropertyName(jsonName);
- JsonSerializer.Serialize(writer, prop.GetValue(value), prop.PropertyType, options);
+ JsonSerializer.Serialize(writer, propValue, prop.PropertyType, options);
}
- foreach (var kvp in extendable.Items)
+ var items = extendable.Items;
+ if (items != null)
{
- writer.WritePropertyName(kvp.Key);
- JsonSerializer.Serialize(writer, kvp.Value, typeof(Object), options);
+ foreach (var kvp in items)
+ {
+ // 扩展字段也遵循全局忽略条件,Object 类型的默认值为 null
+ if (ignoreCondition is JsonIgnoreCondition.WhenWritingNull or JsonIgnoreCondition.WhenWritingDefault && kvp.Value is null) continue;
+
+ writer.WritePropertyName(kvp.Key);
+ JsonSerializer.Serialize(writer, kvp.Value, typeof(Object), options);
+ }
}
writer.WriteEndObject();
}
+
+ /// <summary>判断值是否为类型的默认值</summary>
+ /// <param name="value">属性值</param>
+ /// <param name="type">属性声明类型</param>
+ /// <returns></returns>
+ private static Boolean IsDefaultValue(Object? value, Type type)
+ {
+ if (value is null) return true;
+ if (!type.IsValueType) return false;
+
+ return value.Equals(Activator.CreateInstance(type));
+ }
}
#endif
\ No newline at end of file
diff --git a/XUnitTest.Core/Serialization/ExtendableConverterTests.cs b/XUnitTest.Core/Serialization/ExtendableConverterTests.cs
index 11df48d..7ef3511 100644
--- a/XUnitTest.Core/Serialization/ExtendableConverterTests.cs
+++ b/XUnitTest.Core/Serialization/ExtendableConverterTests.cs
@@ -1,5 +1,6 @@
using System.Text;
using System.Text.Json;
+using System.Text.Json.Serialization;
using NewLife.Data;
using NewLife.Serialization;
using Xunit;
@@ -139,6 +140,70 @@ public class ExtendableConverterTests
Assert.Contains("\"Address\":null", json.Replace(" ", ""));
}
+ [Fact(DisplayName = "Write WhenWritingDefault 跳过 null 和默认值属性")]
+ public void Write_WhenWritingDefault_SkipsDefaultValues()
+ {
+ var opts = new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
+ Converters = { new ExtendableConverter() }
+ };
+
+ // Name 为空字符串(非 null,不是默认值)、Age 为 0(默认值)、Address 为 null(默认值)
+ var model = new ComplexModel { Name = "Bob", Address = null };
+ model.Items["extra"] = "value";
+ model.Items["empty"] = null;
+
+ var json = JsonSerializer.Serialize(model, opts);
+
+ // Name 有值应保留
+ Assert.Contains("\"Name\"", json);
+ // Address 为 null(引用类型默认值)应跳过
+ Assert.DoesNotContain("\"Address\"", json);
+ // Items 中有值的保留
+ Assert.Contains("\"extra\"", json);
+ // Items 中 null 值应跳过
+ Assert.DoesNotContain("\"empty\"", json);
+ }
+
+ [Fact(DisplayName = "Write WhenWritingDefault 跳过值类型默认值")]
+ public void Write_WhenWritingDefault_SkipsValueTypeDefaults()
+ {
+ var opts = new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
+ Converters = { new ExtendableConverter() }
+ };
+
+ var model = new SimpleModel { Name = "Alice", Age = 0 };
+
+ var json = JsonSerializer.Serialize(model, opts);
+
+ // Name 有值应保留
+ Assert.Contains("\"Name\"", json);
+ // Age 为 0(Int32 默认值)应跳过
+ Assert.DoesNotContain("\"Age\"", json);
+ }
+
+ [Fact(DisplayName = "Write WhenWritingNull 仅跳过 null 不跳过值类型默认值")]
+ public void Write_WhenWritingNull_SkipsOnlyNull()
+ {
+ var opts = new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new ExtendableConverter() }
+ };
+
+ var model = new ComplexModel { Name = "Bob", Address = null };
+
+ var json = JsonSerializer.Serialize(model, opts);
+
+ // Name 有值应保留
+ Assert.Contains("\"Name\"", json);
+ // Address 为 null 应跳过
+ Assert.DoesNotContain("\"Address\"", json);
+ }
+
[Fact(DisplayName = "Write 遵循 PropertyNamingPolicy 命名策略")]
public void Write_NamingPolicy_Applied()
{