.NET XmlSerializer反序列化Schema验证时保留Unicode空白字符求助
问题解决方法
带Schema验证的XmlReader会严格遵循W3C XML Schema规范对节点文本值执行空白规范化,自动将制表符(U+0009)、换行符等特殊空白字符替换为普通空格。.NET 5+重构了XmlReader的验证实现,旧版本通过反射修改私有Normalization属性的方案已完全失效,且XmlTextReader的Normalize配置无法覆盖上层验证Reader的规范化逻辑,在无法修改原XML和Schema文件的前提下,可使用以下两种可落地方案:
方案1:拆分验证与反序列化流程(成本最低,推荐优先使用)
不需要把Schema验证和反序列化绑定在同一个XmlReader实例上执行,两个步骤完全可以独立完成,彻底绕开规范化对反序列化结果的影响:
- 第一步单独执行Schema校验:用配置好验证规则的
XmlReader遍历整个XDocument,收集所有验证警告、错误,这一步即使发生空白替换也不会影响源文档内容,仅做合规性检查 - 第二步执行反序列化:直接基于原始XDocument创建不带Schema验证的
XmlReader喂给序列化器,此时读取流程不会触发空白规范化,原始制表符等特殊空白字符可以完整保留
示例代码:
var settings = new XmlReaderSettings { Schemas = schemas, ValidationType = ValidationType.Schema, ValidationFlags = XmlSchemaValidationFlags.ReportValidationWarnings }; // 第一步:单独做Schema验证 List<ValidationEventArgs> validationErrors = new(); settings.ValidationEventHandler += (s, e) => validationErrors.Add(e); using (var validateReader = XmlReader.Create(doc.CreateReader(), settings)) { while (validateReader.Read()) { } // 遍历完整个文档即完成全部校验 } // 按需处理校验错误,比如抛出异常、打日志等 if (validationErrors.Any(e => e.Severity == XmlSeverityType.Error)) { throw new InvalidOperationException("XML格式不符合Schema要求"); } // 第二步:直接用无验证的Reader反序列化,保留原始空白字符 using (var deserializeReader = doc.CreateReader()) { var deserializedObject = serializer.Deserialize(deserializeReader); }
注意:该方案不会出现校验结果和反序列化内容不一致的问题——Schema验证过程仅在读取流时做临时值转换,不会修改传入的XDocument实例的原始内容,两次读取的源数据完全一致。
方案2:自定义包装Reader透传原始节点值(适合必须合并执行验证、反序列化管线的场景)
如果受架构限制不能拆分流程,可以自定义一个继承自XmlReader的包装类,将带验证的Reader作为底层实例,重写所有返回文本值的成员(Value属性、ReadString方法、GetAttribute方法等),读取文本时直接从原始XDocument匹配对应节点拿未修改的原始值,绕过验证Reader返回的规范化结果。
核心实现逻辑参考:
public class RawValueValidatingReader : XmlReader { private readonly XmlReader _innerValidatingReader; private readonly XDocument _sourceDoc; private XObject _currentNode; public RawValueValidatingReader(XDocument source, XmlReaderSettings validationSettings) { _sourceDoc = source; _innerValidatingReader = XmlReader.Create(source.CreateReader(), validationSettings); } // 所有非文本相关的成员直接透传底层验证Reader的实现 public override XmlNodeType NodeType => _innerValidatingReader.NodeType; public override string LocalName => _innerValidatingReader.LocalName; public override string NamespaceURI => _innerValidatingReader.NamespaceURI; public override bool IsEmptyElement => _innerValidatingReader.IsEmptyElement; public override int Depth => _innerValidatingReader.Depth; public override bool EOF => _innerValidatingReader.EOF; public override ReadState ReadState => _innerValidatingReader.ReadState; // 重写文本返回逻辑,优先取原始XDocument的未修改值 public override string Value => _currentNode switch { XText text => text.Value, XAttribute attr => attr.Value, _ => _innerValidatingReader.Value }; public override bool Read() { var readResult = _innerValidatingReader.Read(); if (!readResult) { _currentNode = null; return false; } // 按当前Reader的节点路径匹配源XDocument里的对应节点,赋值给_currentNode var navigator = _sourceDoc.CreateNavigator(); navigator.MoveToFollowing(_innerValidatingReader.LocalName, _innerValidatingReader.NamespaceURI); _currentNode = navigator.UnderlyingObject as XObject; return true; } // 其余需要透传的抽象成员按相同逻辑实现即可,Dispose时释放内层Reader protected override void Dispose(bool disposing) { if (disposing) _innerValidatingReader.Dispose(); base.Dispose(disposing); } }
使用时直接把这个包装类的实例传给序列化器即可,既可以保留Schema验证能力,又能拿到原始的空白字符。
无效方案说明
- 直接使用
XmlTextReader并设置Normalize = false没有效果:该属性仅控制XmlTextReader自身的规范化逻辑,上层叠加的Schema验证Reader会独立执行规范化操作,不受底层Reader配置影响 - 反射修改私有
Normalization属性仅在.NET Framework 4.x及更早版本有效,.NET Core 3.0、.NET 5+重构了验证Reader的实现,该私有字段已被移除,无法生效
内容的提问来源于stack exchange,提问作者Antti Simonen
相关产品推荐
相关产品推荐

