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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 03:42:17