深入掌握 @typespec/xml 装饰器:从属性映射到 XML 命名空间的完整实战指南
2026/9/19 10:57:58 网站建设 项目流程

深入掌握 @typespec/xml 装饰器:从属性映射到 XML 命名空间的完整实战指南

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

导读

@typespec/xml是 TypeSpec 官方提供的 XML 编码库,它通过一组精炼的装饰器(Decorator)控制 TypeSpec 模型在序列化为 XML 时的表现形态:属性是编码为 XML 特性(attribute)还是子节点(node)、元素与属性的最终名称、命名空间(namespace)与前缀(prefix)如何声明,以及列表/文本内容是否需要包装节点。本文以官方参考文档 decorators.md 为核心骨架,结合@typespec/xml包的源码实现与测试用例,逐一拆解 5 个装饰器的签名、约束、底层原理与实战用法,读完即可在自己的 TypeSpec 定义中精确控制 XML 输出结构。


一、装饰器总览

@typespec/xml通过main.tsp(见 main.tsp)聚合导入types.tspdecorators.tsp,对外暴露全部装饰器。其 TypeScript 侧的声明与实现分别位于 decorators.tsp(TypeSpec 声明)与 decorators.ts(运行时实现)。

官方参考文档共收录 5 个装饰器,汇总如下:

装饰器目标类型(Target)参数作用
@TypeSpec.Xml.nameunknown(任意类型)name: valueof string指定 XML 元素/特性的名称,等价于@encodedName("application/xml", value)
@TypeSpec.Xml.attributeModelProperty将目标属性编码为 XML 特性而非子节点
@TypeSpec.Xml.unwrappedModelProperty不为目标属性创建包装节点,用于扁平化数组或内联原始文本
@TypeSpec.Xml.nsunknownns: string \| EnumMemberprefix?: valueof string为元素指定 XML 命名空间与前缀
@TypeSpec.Xml.nsDeclarationsEnum标记某个枚举为 XML 命名空间声明表,配合@ns使用

在 TypeSpec 源文件中,装饰器既可以写全限定名@TypeSpec.Xml.attribute,也可以在使用using TypeSpec.Xml;导入后简写为@attribute。下文示例沿用参考文档与官方测试中的简写形式。


二、@name:精确控制 XML 元素与特性名称

2.1 签名与语义

@TypeSpec.Xml.name(name: valueof string)

@name的语义与@encodedName("application/xml", value)完全等价,即:声明该类型在 XML 编码下的最终名称。它的目标(Target)是unknown,意味着可以作用在模型、模型属性、标量等任意类型上。

官方参考文档中的完整示例:

@name("XmlBook") model Book { @name("XmlId") id: string; @encodedName("application/xml", "XmlName") name: string; content: string; }

序列化结果:

<XmlBook> <XmlId>string</XmlId> <XmlName>string</XmlName> <content>string</content> </XmlBook>

注意示例中id使用@name("XmlId")name使用@encodedName("application/xml", "XmlName"),二者产出完全相同的 XML 元素名,直观印证了两者的等价性;未标注的content则保留原始属性名。

2.2 源码实现:一行转调

@name的实现非常简洁——它本质上是标准@encodedName装饰器在application/xml媒体类型上的语法糖。见 decorators.ts:

export const $name: NameDecorator = (context, target, name) => { context.call($encodedName, target, "application/xml", name); };

2.3 测试验证:多目标支持

官方测试 decorators.test.ts 使用it.each参数化用例,分别验证@name作用于模型模型属性标量三种目标时,resolveEncodedName(program, type, "application/xml")均能解析出指定的XmlName,确认了其Target: unknown的通用性:

it.each([ ["model", `@test @Xml.name("XmlName") model Blob {}`], ["model prop", `model Blob {@Xml.name("XmlName") @test title:string}`], ["scalar", `@Xml.name("XmlName") @test scalar Blob extends string;`], ])("%s", async (_, code) => { const result = await runner.compile(t.code`${code}`); const curr = (result.Blob || result.title) as Model; expect(resolveEncodedName(runner.program, curr, "application/xml")).toEqual("XmlName"); });

三、@attribute:把属性变成 XML 特性

3.1 默认行为 vs 特性编码

XML 中存在两种承载数据的方式:元素子节点(node)特性(attribute)。默认情况下,模型属性会被编码为子节点。参考文档给出的对照示例:

默认编码:

model Blob { id: string; }
<Blob> <id>abcdef</id> </Blob>

使用@attribute之后:

model Blob { @attribute id: string; }
<Blob id="abcdef"> </Blob>

同一份数据,前者是<Blob>下的<id>子节点,后者则是Blob标签上的id="abcdef"特性,XML 文档结构完全不同。

3.2 目标与约束

  • TargetModelProperty,只能作用于模型属性;
  • Parameters:无参数;
  • 互斥约束:参考文档明确指出它不能与@unwrapped同时使用(详见下文第 4 节)。

3.3 源码实现:状态标记

@attribute的实现是把目标属性登记进编译器的状态集合(state set)。见 decorators.ts:

export const $attribute: AttributeDecorator = (context, target) => { context.program.stateSet(XmlStateKeys.attribute).add(target); }; /** Check if the given property should be serialized as an attribute instead of a node. */ export function isAttribute(program: Program, target: ModelProperty): boolean { return program.stateSet(XmlStateKeys.attribute).has(target); }

配套导出的isAttribute()查询函数会被序列化器(各语言 emitter)在输出阶段调用,判断某个属性应当渲染为特性。状态键attribute在库定义 lib.ts 中登记描述为 "Mark a model property to be serialized as xml attribute"。

3.4 测试验证

decorators.test.ts 验证:被@Xml.attribute标注的属性isAttribute(...)返回true,未标注的返回false


四、@unwrapped:去掉包装节点

4.1 作用与互斥约束

@unwrapped指定目标属性不创建包装节点,常用于两种场景:

  1. 扁平化数组节点:让数组元素直接成为外层模型节点的子节点;
  2. 内联原始文本:让字符串内容直接落在模型节点内,而不是嵌套一层子节点。

参考文档明确:它不能与@attribute同时使用(一个属性要么是特性,要么去掉包装,两者语义冲突)。

4.2 数组属性:默认 vs 解包

默认情况下,数组属性会产生一层ItemsTags之类的包装节点(包装节点名称由数组属性名决定):

model Pet { tags: Tag[]; }
<XmlPet> <ItemsTags> <XmlTag> <name>string</name> </XmlTag> </ItemsTags> </XmlPet>

加上@unwrapped后,包装节点被移除,Tag元素直接挂在XmlPet下:

model Pet { @unwrapped tags: Tag[]; }
<XmlPet> <XmlTag> <name>string</name> </XmlTag> </XmlPet>

4.3 字符串属性:默认 vs 内联文本

对于字符串属性,默认会生成一层内容节点:

model BlobName { content: string; }
<BlobName> <content> abcdef </content> </BlobName>

使用@unwrapped后,字符串内容直接成为模型节点的文本内容:

model BlobName { @unwrapped content: string; }
<BlobName> abcdef </BlobName>

4.4 源码实现与测试

@attribute对称,@unwrapped同样基于状态集合实现,见 decorators.ts:

export const $unwrapped: UnwrappedDecorator = (context, target) => { context.program.stateSet(XmlStateKeys.unwrapped).add(target); }; /** Check if the given property should be unwrapped in the XML containing node. */ export function isUnwrapped(program: Program, target: ModelProperty): boolean { return program.stateSet(XmlStateKeys.unwrapped).has(target); }

测试 decorators.test.ts 验证了isUnwrapped()对标注/未标注属性的判别。库定义中状态键描述为 "Mark a model property to be serialized without a node wrapping the content"(见 lib.ts)。


五、@ns@nsDeclarations:XML 命名空间体系

XML 命名空间是跨文档共享元素定义的基础设施,@typespec/xml提供了两套声明方式。

5.1@ns的两种用法

签名如下(参考文档原样):

@TypeSpec.Xml.ns(ns: string | EnumMember, prefix?: valueof string)
参数类型说明
nsstring \| EnumMember命名空间 URI,或一个被@nsDeclaration装饰的枚举的成员
prefixvalueof string命名空间前缀;ns以字符串传入时为必填

用法一:字符串 URI + 前缀

@ns("https://example.com/ns1", "ns1") model Foo { @ns("https://example.com/ns1", "ns1") bar: string; @ns("https://example.com/ns2", "ns2") bar: string; }

用法二:引用@nsDeclarations枚举成员

@Xml.nsDeclarations enum Namespaces { ns1: "https://example.com/ns1", ns2: "https://example.com/ns2", } @Xml.ns(Namespaces.ns1) model Foo { @Xml.ns(Namespaces.ns1) bar: string; @Xml.ns(Namespaces.ns2) bar: string; }

采用枚举方式时,前缀自动取枚举成员名(如ns1ns2),无需也不能再手动传前缀。

5.2@nsDeclarations:命名空间声明表

@TypeSpec.Xml.nsDeclarations
  • TargetEnum
  • Parameters:无;
  • 作用:将枚举标记为 XML 命名空间声明表。枚举成员的值必须是命名空间 URI 字符串,成员名即前缀。

5.3 源码实现与 5 类诊断规则

@ns的参数处理集中在 decorators.ts 的getData()中,其分支逻辑与错误校验清晰对应着参考文档的约束:

  1. 字符串 URI 必须携带前缀——缺失时抛出ns-missing-prefix错误:"When using a string namespace you must provide a prefix as the 2nd argument.";
  2. 枚举成员必须来自@nsDeclarations枚举——否则抛出ns-enum-not-declaration:"Enum member used as namespace must be part of an enum marked with @nsDeclaration.";
  3. 枚举成员值必须是字符串 URI——值为空或数字时抛出invalid-ns-declaration-member:"Enum membernamemust have a value that is the XML namespace url.";
  4. 枚举方式禁止再传前缀——抛出prefix-not-allowed:"@ns decorator cannot have the prefix parameter set when using an enum member.";
  5. 命名空间必须是合法 URI——通过new URL(namespace)校验(见 validateNamespaceIsUri),非法时抛出ns-not-uri:"Namespacenamespaceis not a valid URI."。

上述诊断信息全部登记在库定义 lib.ts 中,错误码格式为@typespec/xml/<code>

解析成功后,命名空间以{ namespace, prefix }结构存入状态映射(state map),数据模型即 types.ts 中的XmlNamespace接口:

export interface XmlNamespace { /** Namespace name */ readonly namespace: string; /** Namespace prefix */ readonly prefix: string; }

外部查询通过getNs(program, target)获取(见 decorators.ts)。

5.4 测试验证:命名空间不向子级传递

decorators.test.ts 对@ns进行了全面覆盖,值得注意的用例包括:

  • 字符串方式与枚举方式都能正确解析出{ namespace, prefix }
  • 命名空间不会自动向子级传递:在Blob模型上标注@ns,其属性id查询getNs返回undefined,说明命名空间需要逐级显式声明;
  • 上述 5 类错误场景均有对应诊断测试,例如缺少第二参、给枚举方式传前缀、命名空间不是合法 URL 等。

六、综合实战示例

将 5 个装饰器组合使用,可以得到一份完整的 XML 映射模型(综合参考文档示例整理):

using TypeSpec.Xml; @nsDeclarations enum Namespaces { storage: "https://example.com/storage", } @name("XmlBook") @ns(Namespaces.storage) model Book { @name("XmlId") @attribute id: string; @name("XmlName") name: string; @unwrapped tags: string[]; @unwrapped content: string; }

对应输出 XML 的形态如下(示意):

<XmlBook xmlns:storage="https://example.com/storage" storage:XmlId="string"> <storage:XmlName>string</storage:XmlName> <string>tag1</string> <string>tag2</string> string </XmlBook>

要点回顾:

  • @name负责元素/特性命名,@attributeid成为特性;
  • @ns+@nsDeclarations声明命名空间与前缀;
  • @unwrapped分别将数组元素直接平铺、将文本内容内联进模型节点。

七、验证与运行方式

仓库为@typespec/xml提供了完整的单元测试,核心位于 decorators.test.ts,涵盖@name@attribute@unwrapped@ns@nsDeclarations的全部行为与诊断分支。测试基于 test-host.ts 构建的Tester实例,并使用@typespec/compiler/testing提供的t测试辅助与expectDiagnostics断言工具。感兴趣的读者可以在仓库根目录执行对应包的测试命令(参见 package.json)进行本地验证。

此外,@typespec/xml还定义了 XML 编码枚举TypeSpec.Xml.EncodingxmlDateTimexmlDatexmlTimexmlDurationxmlBase64Binary,见 types.tsp),配合编译器内置的@encode装饰器使用,相关编码实现位于 encoding.ts,可作为理解该库完整能力的下一步阅读材料。


结语

@typespec/xml的装饰器设计遵循"少而精"的原则:@name统一命名、@attribute控制特性形态、@unwrapped控制节点层级、@ns/@nsDeclarations管理命名空间。理解它们各自的 Target、参数与底层状态存储机制(state set / state map),以及 5 类编译期诊断规则,就能在编写 TypeSpec 时精准预测 XML 输出,为各语言 emitter 的正确序列化打下坚实基础。完整的签名、参数表与示例可随时查阅官方参考文档 decorators.md,以及源码 decorators.ts 与测试 decorators.test.ts。

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询