TypeSpec OpenAPI3 数据模型指南:ExternalDocs 与 TagMetadata 类型详解
2026/9/18 10:19:40 网站建设 项目流程

TypeSpec OpenAPI3 数据模型指南:ExternalDocs 与 TagMetadata 类型详解

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

导读

TypeSpec 编译器在将服务定义发射为 OpenAPI 3 文档时,会用到一套预定义的元数据类型来承载"外部文档链接"和"标签元信息"。本文以@typespec/openapi3参考文档中的TypeSpec.OpenAPI命名空间数据模型(ExternalDocsTagMetadata)为核心,深入讲解这两个模型的字段含义、@externalDocs@tagMetadata装饰器的实际用法,并结合仓库源码与测试用例剖析它们在 OpenAPI 3.0/3.1/3.2 各版本下的发射行为。读完本文,你将能够为操作、模型、属性附加外部文档链接,为 API 标签配置描述、父子层级与扩展字段,并理解 OpenAPI3 发射器的底层实现。

背景:TypeSpec.OpenAPI命名空间中的数据模型

在 TypeSpec 生态中,OpenAPI 相关能力被拆分为两个包:

  • @typespec/openapi:提供通用 OpenAPI 元数据装饰器与类型定义(@externalDocs@tagMetadata@info等),属于底层基础库;
  • @typespec/openapi3:消费上述装饰器,最终将 TypeSpec 程序发射为 OpenAPI 3.0/3.1/3.2 文档。

本文涉及的ExternalDocsTagMetadata两个数据模型,在参考文档中归属于TypeSpec.OpenAPI命名空间,实际由@typespec/openapi库声明并实现(见 packages/openapi/lib/decorators.tsp),并被 openapi3 发射器读取、映射为 OpenAPI 规范中的externalDocs对象与tags数组条目。

ExternalDocs:外部文档信息模型

模型定义与字段

参考文档给出了TypeSpec.OpenAPI.ExternalDocs的模型签名:

model TypeSpec.OpenAPI.ExternalDocs

字段如下:

名称类型说明
urlstring文档链接地址(Documentation url)
description?string可选描述(Optional description)

对照源码 packages/openapi/lib/decorators.tsp 中的定义,该模型还支持通过...Record<unknown>携带自定义扩展字段(键必须以x-开头):

/** External Docs information. */ model ExternalDocs { /** Documentation url */ url: string; /** Optional description */ description?: string; /** Attach some custom data, The extension key must start with `x-`. */ ...Record<unknown>; }

在 TypeScript 侧,@typespec/openapi用同名接口表达该结构,见 packages/openapi/src/types.ts:

export interface ExternalDocs { /** Documentation url */ url: string; /** Optional description */ description?: string; }

openapi3 发射器在输出阶段将其映射为 OpenAPI 规范对象(packages/openapi3/src/types.ts):

export interface OpenAPI3ExternalDocs { url: string; description?: string; }

注意:url为必填字段,且校验要求其必须是合法 URL(见下文@externalDocs装饰器实现)。

@externalDocs装饰器:如何设置外部文档

ExternalDocs模型本身不直接出现在用户代码中,而是通过@externalDocs装饰器产生。装饰器声明位于 packages/openapi/lib/decorators.tsp:

/** * Specify the OpenAPI `externalDocs` property for this type. * * @param url Url to the docs * @param description Description of the docs * * @example * ```typespec * @externalDocs("https://example.com/detailed.md", "Detailed information on how to use this operation") * op listPets(): Pet[]; * ``` */ extern dec externalDocs(target: unknown, url: valueof string, description?: valueof string);

target类型为unknown,意味着它可以作用于任意类型。其底层实现见 packages/openapi/src/decorators.ts:

const externalDocsKey = createStateSymbol("externalDocs"); export const $externalDocs: ExternalDocsDecorator = ( context: DecoratorContext, target: Type, url: string, description?: string, ) => { const doc: ExternalDocs = { url }; if (description) { doc.description = description; } context.program.stateMap(externalDocsKey).set(target, doc); }; export function getExternalDocs(program: Program, entity: Type): ExternalDocs | undefined { return program.stateMap(externalDocsKey).get(entity); }

从源码结构可以看出其工作方式:

  1. 装饰器把{ url, description? }存入编译器的状态映射(stateMap),键为目标类型;
  2. 发射阶段通过getExternalDocs查询任意类型的关联文档信息;
  3. description仅在有值时才写入结果,未提供时结果对象只含url

实操:为操作、模型、属性附加外部文档

根据测试文件 packages/openapi3/test/documentation.test.ts,@externalDocs可以作用在操作、模型、模型属性上:

// 作用在操作上:输出到 paths["/"].get.externalDocs @externalDocs("https://example.com", "more info") op read(): {}; // 作用在模型上:输出到 components.schemas.Foo.externalDocs @externalDocs("https://example.com", "more info") model Foo { name: string; } // 作用在属性上:输出到 components.schemas.Foo.properties.name.externalDocs model Foo { @externalDocs("https://example.com", "more info") name: string; }

对应发射结果(来自同一测试文件):

// 操作级 { "url": "https://example.com", "description": "more info" } // 模型级 { "url": "https://example.com", "description": "more info" } // 属性级(简单 string 类型) { "type": "string", "externalDocs": { "url": "https://example.com", "description": "more info" } }

边界行为:与$ref的交互

当属性引用的类型需要以$ref形式输出时,行为随 OpenAPI 版本而不同。测试 packages/openapi3/test/documentation.test.ts 验证了这一点:

model Foo { @externalDocs("https://example.com", "more info") name: Bar; } model Bar {}
  • OpenAPI 3.0:规范不允许在$ref旁携带其他关键字,因此发射器将引用包装在allOf中,再把externalDocs放在同一层级:
    { "allOf": [{ "$ref": "#/components/schemas/Bar" }], "externalDocs": { "url": "https://example.com", "description": "more info" } }
  • OpenAPI 3.1+$ref可以与其他字段并存,直接输出:
    { "$ref": "#/components/schemas/Bar", "externalDocs": { "url": "https://example.com", "description": "more info" } }

这一版本差异由发射器中的引用处理逻辑自动完成,用户无需感知。

底层发射链路

externalDocs的注入分散在 openapi3 发射器的多个环节:

  • 操作与共享路由applyExternalDocs(op, oai3Operation)在 packages/openapi3/src/openapi.ts(单操作)与 packages/openapi3/src/openapi.ts(共享路由,多个操作合并时逐一应用)被调用;
  • 模型与属性:Schema 发射器在 packages/openapi3/src/schema-emitter.ts(模型)与 packages/openapi3/src/schema-emitter.ts(属性)中调用#applyExternalDocs
  • 服务级getExternalDocs还被用于根级文档信息,见 packages/openapi3/src/openapi-spec-mappings.ts。

核心辅助函数定义如下(packages/openapi3/src/openapi.ts):

function applyExternalDocs(typespecType: Type, target: Record<string, unknown>) { const externalDocs = getExternalDocs(program, typespecType); if (externalDocs) { target.externalDocs = externalDocs; } }

TagMetadata:标签元数据模型

模型定义与字段

参考文档给出了TypeSpec.OpenAPI.TagMetadata的模型签名与两个字段:

model TypeSpec.OpenAPI.TagMetadata
名称类型说明
description?stringAPI 的描述
externalDocs?ExternalDocsAPI 的外部文档信息

需要特别指出的是,源码中的TagMetadata字段远不止这两个。完整的模型定义在 packages/openapi/lib/decorators.tsp:

/** Metadata to a single tag that is used by operations. */ model TagMetadata { /** A description of the tag. */ description?: string; /** External documentation information for the tag. */ externalDocs?: ExternalDocs; /** The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. */ parent?: string; /** A short summary of the tag, used for display purposes. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-summary`. */ summary?: string; /** A machine-readable string to categorize what sort of tag it is. Any string value can be used. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-kind`. */ kind?: string; /** Attach some custom data, The extension key must start with `x-`. */ ...Record<unknown>; }

descriptionexternalDocs外,还支持:

字段说明OpenAPI 3.0/3.1 下的发射形态
parent父标签名,用于标签嵌套转换为x-oai-parent
summary标签的简短摘要(展示用途)转换为x-oai-summary
kind机器可读的标签类别字符串转换为x-oai-kind
x-*扩展字段自定义数据,键必须以x-开头原样透传

配套还有TagMetadataWithName模型(packages/openapi/lib/decorators.tsp),它继承全部TagMetadata字段并额外携带必填的name

model TagMetadataWithName { /** The name of the tag. */ name: string; ...TagMetadata; }

TypeScript 侧的对应接口见 packages/openapi/src/types.ts。

@tagMetadata装饰器:两种调用形式

@tagMetadata装饰器作用于服务命名空间(必须有@service标记),支持两种形式(packages/openapi/lib/decorators.tsp):

extern dec tagMetadata( target: Namespace, name: valueof string | TagMetadataWithName[], tagMetadata?: valueof TagMetadata );

内联形式:为单个标签配置元数据。

@service() @tagMetadata("Tag Name", #{description: "Tag description", externalDocs: #{url: "https://example.com", description: "More info.", `x-custom`: "string"}, `x-custom`: "string"}) @tagMetadata("Child Tag", #{description: "Child tag description", parent: "Tag Name"}) namespace PetStore {}

数组形式:一次调用声明多个标签,并严格保持声明顺序

@service() @tagMetadata(#[ #{ name: "First Tag", description: "First tag description" }, #{ name: "Second Tag", description: "Second tag description" }, ]) namespace PetStore {}

装饰器底层实现与校验逻辑

tagMetadataDecorator的实现位于 packages/openapi/src/decorators.ts,核心逻辑包括:

  1. 服务校验:目标命名空间必须带有@service装饰器,否则报tag-metadata-target-service诊断错误;
  2. 形式互斥:内联形式与数组形式不能混用(mixed-tag-metadata-form),同一命名空间上数组形式只能调用一次;
  3. 去重:重复的标签名会触发duplicate-tag诊断;
  4. 模型校验:元数据对象需符合TypeSpec.OpenAPI.TagMetadata/TagMetadataWithName模型约束(validateAdditionalInfoModel);
  5. URL 校验externalDocs.url必须是合法 URI(validateIsUri),否则报错;
  6. 存储:最终以TagMetadataWithName[]的形式存入状态映射,供发射阶段读取。

发射行为:从 TypeSpec 到 OpenAPI 的 tags 数组

openapi3 发射器通过resolveDocumentTags生成文档根级的tags数组(packages/openapi3/src/openapi.ts):

function resolveDocumentTags(service: Service): OpenAPI3Tag[] | OpenAPITag3_2[] { const metadataList = getTagsMetadata(program, service.type); const metadataByName = new Map(metadataList?.map((t) => [t.name, t])); const tags: OpenAPI3Tag[] | OpenAPITag3_2[] = []; for (const tag of tagsUsedInOperations) { if (!metadataByName.has(tag)) { tags.push({ name: tag }); } } for (const tag of metadataList ?? []) { const { name, ...rest } = tag; const tagData: OpenAPI3Tag = { name, ...rest }; // 对于 OpenAPI 3.0 和 3.1,将 parent、summary、kind 转换为 x-oai- 前缀的扩展 if (specVersion !== "3.2.0") { // parent -> x-oai-parent, summary -> x-oai-summary, kind -> x-oai-kind } tags.push(tagData); } return tags; }

该函数揭示了重要的排序与去重规则:

  • 仅出现在操作上的标签(未在@tagMetadata中定义)会以{ name }的裸形式先行输出
  • @tagMetadata声明的标签随后按声明顺序输出,并携带完整元数据;
  • 操作级标签与@tagMetadata标签重名时不会重复输出,只输出带元数据的版本。

tagsUsedInOperations集合在生成每个操作时被填充(packages/openapi3/src/openapi.ts 与 packages/openapi3/src/openapi.ts):操作上所有@tag标记都会被收集,同时写入oai3Operation.tags

版本差异:parentsummarykind的兼容处理

这三个字段是 OpenAPI 3.2 新增的原生字段,对于 3.0/3.1 目标版本,发射器自动降级为x-oai-前缀的扩展字段。测试文件 packages/openapi3/test/tagmetadata.test.ts 用多组用例验证了全部组合:

OpenAPI 3.2(原生字段原样输出):

@service @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) namespace PetStore { @tag("foo") op test(): string; }
{ "name": "foo", "summary": "all operations that allow doing Foo", "kind": "FooGroup" }

OpenAPI 3.0/3.1(转换为扩展字段):

{ "name": "foo", "x-oai-summary": "all operations that allow doing Foo", "x-oai-kind": "FooGroup" }

父标签(parent):3.2 直接输出parent,3.0/3.1 输出x-oai-parent

// 3.2 { "name": "ChildTag", "description": "Child tag", "parent": "ParentTag" } // 3.0/3.1 { "name": "ChildTag", "description": "Child tag", "x-oai-parent": "ParentTag" }

装饰器执行顺序:内联形式多个@tagMetadata叠加时,装饰器自底向上执行,因此靠实体更近(写在下方)的标签先存储。例如:

@tagMetadata("ParentTag", #{description: "Parent tag"}) @tagMetadata("ChildTag", #{description: "Child tag", parent: "ParentTag"})

输出顺序为ChildTag在前、ParentTag在后(packages/openapi3/test/tagmetadata.test.ts)。若需严格控制顺序,应使用数组形式。

完整实操示例:结合 externalDocs 与 tagMetadata

综合@externalDocs@tag@tagMetadata,一个完整的服务定义如下:

import "@typespec/http"; import "@typespec/openapi3"; @service({ title: "PetStore", }) @tagMetadata("Pets", #{ description: "All operations about pets", externalDocs: #{ url: "https://example.com/pets-doc", description: "Detailed pet documentation", `x-source`: "internal-wiki", }, `x-category`: "animals", }) namespace PetStore; @tag("Pets") @externalDocs("https://example.com/pets-doc/operations", "How to use pet operations") op listPets(): string[];

发射后根级tags大致为:

{ "tags": [ { "name": "Pets", "description": "All operations about pets", "externalDocs": { "url": "https://example.com/pets-doc", "description": "Detailed pet documentation", "x-source": "internal-wiki" }, "x-category": "animals" } ], "paths": { "/": { "get": { "tags": ["Pets"], "externalDocs": { "url": "https://example.com/pets-doc/operations", "description": "How to use pet operations" } } } } }

测试覆盖验证

@typespec/openapi3针对本主题提供了系统化测试:

  • packages/openapi3/test/documentation.test.ts:覆盖externalDocs在操作、模型、属性及$ref场景下的全部行为;
  • packages/openapi3/test/tagmetadata.test.ts:覆盖tagMetadata内联/数组形式、多服务隔离、排序去重、parent/summary/kind 的版本兼容、混合形式报错等场景;
  • packages/openapi3/test/info.test.ts:覆盖服务级@externalDocs输出到文档根级的场景。

小结

  • ExternalDocsurl(必填)与description(可选)构成,可携带x-*扩展字段,通过@externalDocs装饰器挂载到操作、模型、属性乃至服务上,发射为对应 OpenAPI 节点的externalDocs
  • TagMetadatadescriptionexternalDocs外,还支持parentsummarykind与扩展字段;通过@tagMetadata内联形式数组形式声明,数组形式能精确控制tags数组顺序;
  • parentsummarykind仅在 OpenAPI 3.2 中原生输出,面向 3.0/3.1 会自动转为x-oai-parent/x-oai-summary/x-oai-kind
  • 底层实现依赖@typespec/openapi的状态映射(stateMap)存储元数据,openapi3 发射器在 openapi.ts 与 schema-emitter.ts 中消费并映射到 OpenAPI 文档。

更多相关参考:@typespec/openapi3的装饰器参考见 decorators.md,发射器配置见 emitter.md。

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

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

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

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

立即咨询