From 9c82564748a2c82d14475f158a6580576a1e2283 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 20 May 2025 12:45:27 -0700
Subject: [PATCH 01/14] [openapi3] add support for parameter examples via
`@opExample`
---
packages/openapi3/src/examples.ts | 383 +++++++++++++++++++++++-
packages/openapi3/src/openapi.ts | 39 ++-
packages/openapi3/src/parameters.ts | 16 +
packages/openapi3/src/types.ts | 5 +
packages/openapi3/src/util.ts | 13 +-
packages/openapi3/test/examples.test.ts | 382 ++++++++++++++++++++++-
6 files changed, 812 insertions(+), 26 deletions(-)
create mode 100644 packages/openapi3/src/parameters.ts
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index ffdc70e9346..1499c1fe023 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -1,13 +1,17 @@
import {
+ BooleanValue,
Example,
getOpExamples,
ignoreDiagnostics,
+ NumericValue,
OpExample,
Program,
serializeValueAsJson,
+ StringValue,
Type,
Value,
} from "@typespec/compiler";
+import { $ } from "@typespec/compiler/typekit";
import type {
HttpOperation,
HttpOperationResponse,
@@ -15,12 +19,19 @@ import type {
HttpProperty,
HttpStatusCodeRange,
} from "@typespec/http";
+import { getParameterStyle } from "./parameters.js";
import { getOpenAPI3StatusCodes } from "./status-codes.js";
import { OpenAPI3Example, OpenAPI3MediaType } from "./types.js";
-import { isSharedHttpOperation, SharedHttpOperation } from "./util.js";
+import {
+ HttpParameterProperties,
+ isHttpParameterProperty,
+ isSharedHttpOperation,
+ SharedHttpOperation,
+} from "./util.js";
export interface OperationExamples {
requestBody: Record;
+ parameters: Record;
responses: Record>;
}
@@ -29,7 +40,7 @@ export function resolveOperationExamples(
operation: HttpOperation | SharedHttpOperation,
): OperationExamples {
const examples = findOperationExamples(program, operation);
- const result: OperationExamples = { requestBody: {}, responses: {} };
+ const result: OperationExamples = { requestBody: {}, parameters: {}, responses: {} };
if (examples.length === 0) {
return result;
}
@@ -50,6 +61,28 @@ export function resolveOperationExamples(
]);
}
}
+
+ if (example.parameters) {
+ // iterate over properties
+ for (const property of op.parameters.properties) {
+ if (!isHttpParameterProperty(property)) continue;
+
+ const value = getParameterValue(program, example.parameters, property);
+ if (value) {
+ const parameterName = property.options.name;
+ result.parameters[parameterName] ??= [];
+ result.parameters[parameterName].push([
+ {
+ value,
+ title: example.title,
+ description: example.description,
+ },
+ property.property as Type,
+ ]);
+ }
+ }
+ }
+
if (example.returnType && op.responses) {
const match = findResponseForExample(program, example.returnType, op.responses);
if (match) {
@@ -231,6 +264,352 @@ export function getBodyValue(value: Value, properties: HttpProperty[]): Value |
return value;
}
+function getParameterValue(
+ program: Program,
+ parameterExamples: Value,
+ property: HttpParameterProperties,
+): Value | undefined {
+ const value = getValueByPath(parameterExamples, property.path);
+ if (!value) return value;
+
+ // Depending on the parameter type, we may need to serialize the value differently.
+ // https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ /*
+ Supported styles per location: https://spec.openapis.org/oas/v3.0.4.html#style-values
+ | Location | Default Style | Supported Styles |
+ | -------- | ------------- | ----------------------------------------------- |
+ | query | form | form, spaceDelimited, pipeDelimited, deepObject |
+ | header | simple | simple |
+ | path | simple | simple, label, matrix |
+ | cookie | form | form |
+ */
+ // explode is only relevant for array/object types
+
+ if (property.kind === "query") {
+ return getQueryParameterValue(program, value, property);
+ } else if (property.kind === "header") {
+ return getHeaderParameterValue(program, value, property);
+ } else if (property.kind === "path") {
+ return getPathParameterValue(program, value, property);
+ } else if (property.kind === "cookie") {
+ return getCookieParameterValue(program, value, property);
+ }
+
+ return value;
+}
+
+function getQueryParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const style = getParameterStyle(program, property.property) ?? "form";
+
+ switch (style) {
+ case "form":
+ return getParameterFormValue(program, originalValue, property);
+ case "spaceDelimited":
+ return getParameterDelimitedValue(program, originalValue, property, " ");
+ case "pipeDelimited":
+ return getParameterDelimitedValue(program, originalValue, property, "|");
+ }
+}
+
+function getHeaderParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ return getParameterSimpleValue(program, originalValue, property);
+}
+
+function getPathParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { style } = property.options;
+
+ if (style === "label") {
+ return getParameterLabelValue(program, originalValue, property);
+ } else if (style === "matrix") {
+ return getParameterMatrixValue(program, originalValue, property);
+ } else if (style === "simple") {
+ return getParameterSimpleValue(program, originalValue, property);
+ }
+
+ return undefined;
+}
+
+function getCookieParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ return getParameterFormValue(program, originalValue, property);
+}
+
+function getParameterLabelValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { explode } = property.options;
+ const tk = $(program);
+
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------- | ----------------- | ------------------ |
+ | false | .blue | .blue,black,brown | .R,100,G,200,B,150 |
+ | true | .blue | .blue.black.brown | .R=100.G=200.B=150 |
+ */
+
+ const joiner = explode ? "." : ",";
+ if (tk.value.isArray(originalValue)) {
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${value.value}`);
+ }
+ return tk.value.createString(`.${pairs.join(joiner)}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ const sep = explode ? "=" : ",";
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(`.${pairs.join(joiner)}`);
+ }
+
+ // null (undefined) is treated as a a dot
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString(".");
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return tk.value.createString(`.${originalValue.value}`);
+ }
+
+ return;
+}
+
+function getParameterMatrixValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { explode, name } = property.options;
+ const tk = $(program);
+
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------------- | ----------------------------------- | ------------------------ |
+ | false | ;color=blue | ;color=blue,black,brown | ;color=R,100,G,200,B,150 |
+ | true | ;color=blue | ;color=blue;color=black;color=brown | ;R=100;G=200;B=150 |
+ */
+
+ const joiner = explode ? ";" : ",";
+ const prefix = explode ? "" : `${name}=`;
+ if (tk.value.isArray(originalValue)) {
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
+ }
+ return tk.value.createString(`;${prefix}${pairs.join(joiner)}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const sep = explode ? "=" : ",";
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(`;${prefix}${pairs.join(joiner)}`);
+ }
+
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString(`;${name}`);
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return tk.value.createString(`;${name}=${originalValue.value}`);
+ }
+
+ return;
+}
+
+function getParameterDelimitedValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+ delimiter: " " | "|",
+): Value | undefined {
+ const { explode, name } = property.options;
+ // Serialization is undefined for explode=true
+ if (explode) return undefined;
+
+ const tk = $(program);
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | style | explode | string | array | object |
+ | ----- | ------- | ------ | --------------------------- | ---------------------------------- |
+ | pipe | false | n/a | ?color=blue%7Cblack%7Cbrown | ?color=R%7C100%7CG%7C200%7CB%7C150 |
+ | pipe | true | n/a | n/a | n/a |
+ | space | false | n/a | ?color=blue%20black%20brown | ?color=R%20100%20G%20200%20B%20150 |
+ | space | true | n/a | n/a | n/a |
+ */
+
+ if (tk.value.isArray(originalValue)) {
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${value.value}`);
+ }
+ return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${key}${delimiter}${value.value}`);
+ }
+ return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
+ }
+
+ return undefined;
+}
+
+function getParameterFormValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { name } = property.options;
+ const isCookie = property.kind === "cookie";
+ const explode = isCookie ? false : property.options.explode;
+ const tk = $(program);
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------------- | ----------------------------------- | ------------------------ |
+ | false | ?color=blue | ?color=blue,black,brown | ?color=R,100,G,200,B,150 |
+ | true | ?color=blue | ?color=blue&color=black&color=brown | ?R=100&G=200&B=150 |
+ */
+
+ const qPrefix = isCookie ? "" : "?";
+ const prefix = explode ? "" : `${name}=`;
+ if (tk.value.isArray(originalValue)) {
+ const sep = explode ? "&" : ",";
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
+ }
+ return tk.value.createString(`${qPrefix}${prefix}${pairs.join(sep)}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const sep = explode ? "=" : ",";
+ const joiner = explode ? "&" : ",";
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(`${qPrefix}${prefix}${pairs.join(joiner)}`);
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return tk.value.createString(`${qPrefix}${name}=${originalValue.value}`);
+ }
+
+ // null is treated as the 'undefined' value
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString(`${qPrefix}${name}=`);
+ }
+
+ return;
+}
+
+function getParameterSimpleValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { explode } = property.options;
+ const tk = $(program);
+
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------ | ---------------- | ----------------- |
+ | false | blue | blue,black,brown | R,100,G,200,B,150 |
+ | true | blue | blue,black,brown | R=100,G=200,B=150 |
+ */
+
+ if (tk.value.isArray(originalValue)) {
+ const serializedValue = originalValue.values
+ .filter(isSerializableScalarValue)
+ .map((v) => v.value)
+ .join(",");
+ return tk.value.createString(serializedValue);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ const sep = explode ? "=" : ",";
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(pairs.join(","));
+ }
+
+ // null (undefined) is treated as an empty string - unrelated to allowEmptyValue
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString("");
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return originalValue;
+ }
+
+ return;
+}
+
+function isSerializableScalarValue(
+ value: Value,
+): value is BooleanValue | NumericValue | StringValue {
+ return ["BooleanValue", "NumericValue", "StringValue"].includes(value.valueKind);
+}
+
function getValueByPath(value: Value, path: (string | number)[]): Value | undefined {
let current: Value | undefined = value;
for (const key of path) {
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index 86739504387..74d22b16a23 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -10,7 +10,6 @@ import {
getAllTags,
getAnyExtensionFromPath,
getDoc,
- getEncode,
getFormat,
getMaxItems,
getMaxLength,
@@ -88,6 +87,7 @@ import { getExampleOrExamples, OperationExamples, resolveOperationExamples } fro
import { JsonSchemaModule, resolveJsonSchemaModule } from "./json-schema.js";
import { createDiagnostic, FileType, OpenAPI3EmitterOptions, OpenAPIVersion } from "./lib.js";
import { getOpenApiSpecProps } from "./openapi-spec-mappings.js";
+import { getParameterStyle } from "./parameters.js";
import { getOpenAPI3StatusCodes } from "./status-codes.js";
import {
OpenAPI3Encoding,
@@ -115,6 +115,7 @@ import {
deepEquals,
ensureValidComponentFixedFieldKey,
getDefaultValue,
+ HttpParameterProperties,
isBytesKeptRaw,
isSharedHttpOperation,
SharedHttpOperation,
@@ -142,11 +143,6 @@ export async function $onEmit(context: EmitContext) {
type IrrelevantOpenAPI3EmitterOptionsForObject = "file-type" | "output-file" | "new-line";
-type HttpParameterProperties = Extract<
- HttpProperty,
- { kind: "header" | "query" | "path" | "cookie" }
->;
-
/**
* Get the OpenAPI 3 document records from the given program. The documents are
* returned as a JS object.
@@ -787,6 +783,7 @@ function createOAPIEmitter(
oai3Operation.parameters = getEndpointParameters(
resolveSharedRouteParameters(operations),
visibility,
+ examples,
);
const bodies = [
@@ -818,7 +815,7 @@ function createOAPIEmitter(
operationId: resolveOperationId(program, operation.operation),
summary: getSummary(program, operation.operation),
description: getDoc(program, operation.operation),
- parameters: getEndpointParameters(parameters.properties, visibility),
+ parameters: getEndpointParameters(parameters.properties, visibility, examples),
responses: getResponses(operation, operation.responses, examples),
};
const currentTags = getAllTags(program, op);
@@ -1281,6 +1278,7 @@ function createOAPIEmitter(
function getParameter(
httpProperty: HttpParameterProperties,
visibility: Visibility,
+ examples: [Example, Type][],
): OpenAPI3Parameter {
const param: OpenAPI3Parameter = {
name: httpProperty.options.name,
@@ -1301,12 +1299,16 @@ function createOAPIEmitter(
param.deprecated = true;
}
+ const paramExamples = getExampleOrExamples(program, examples);
+ Object.assign(param, paramExamples);
+
return param;
}
function getEndpointParameters(
properties: HttpProperty[],
visibility: Visibility,
+ examples: OperationExamples,
): Refable[] {
const result: Refable[] = [];
for (const httpProp of properties) {
@@ -1317,7 +1319,11 @@ function createOAPIEmitter(
if (!isHttpParameterProperty(httpProp)) {
continue;
}
- const param = getParameterOrRef(httpProp, visibility);
+ const param = getParameterOrRef(
+ httpProp,
+ visibility,
+ examples.parameters[httpProp.options.name] ?? [],
+ );
if (param) {
const existing = result.find(
(x) => !("$ref" in param) && !("$ref" in x) && x.name === param.name && x.in === param.in,
@@ -1386,6 +1392,7 @@ function createOAPIEmitter(
function getParameterOrRef(
httpProperty: HttpParameterProperties,
visibility: Visibility,
+ examples: [Example, Type][],
): Refable | undefined {
if (isNeverType(httpProperty.property.type)) {
return undefined;
@@ -1415,7 +1422,7 @@ function createOAPIEmitter(
return params.get(property);
}
- const param = getParameter(httpProperty, visibility);
+ const param = getParameter(httpProperty, visibility, examples);
// only parameters inherited by spreading from non-inlined type are shared in #/components/parameters
if (spreadParam && property.model && !shouldInline(program, property.model)) {
@@ -1546,7 +1553,7 @@ function createOAPIEmitter(
// For query parameters(style: form) the default is explode: true https://spec.openapis.org/oas/v3.0.2#fixed-fields-9
attributes.explode = false;
}
- const style = getParameterStyle(httpProperty.property);
+ const style = getParameterStyle(program, httpProperty.property);
if (style) {
attributes.style = style;
}
@@ -1554,18 +1561,6 @@ function createOAPIEmitter(
return attributes;
}
- function getParameterStyle(type: ModelProperty): string | undefined {
- const encode = getEncode(program, type);
- if (!encode) return;
-
- if (encode.encoding === "ArrayEncoding.pipeDelimited") {
- return "pipeDelimited";
- } else if (encode.encoding === "ArrayEncoding.spaceDelimited") {
- return "spaceDelimited";
- }
- return;
- }
-
function getHeaderParameterAttributes(httpProperty: HttpProperty & { kind: "header" }) {
const attributes: { style?: "simple"; explode?: boolean } = {};
if (httpProperty.options.explode) {
diff --git a/packages/openapi3/src/parameters.ts b/packages/openapi3/src/parameters.ts
new file mode 100644
index 00000000000..d2c22a753e2
--- /dev/null
+++ b/packages/openapi3/src/parameters.ts
@@ -0,0 +1,16 @@
+import { getEncode, ModelProperty, Program } from "@typespec/compiler";
+
+export function getParameterStyle(
+ program: Program,
+ type: ModelProperty,
+): "pipeDelimited" | "spaceDelimited" | undefined {
+ const encode = getEncode(program, type);
+ if (!encode) return;
+
+ if (encode.encoding === "ArrayEncoding.pipeDelimited") {
+ return "pipeDelimited";
+ } else if (encode.encoding === "ArrayEncoding.spaceDelimited") {
+ return "spaceDelimited";
+ }
+ return;
+}
diff --git a/packages/openapi3/src/types.ts b/packages/openapi3/src/types.ts
index 4b87710afa1..7d18c85644f 100644
--- a/packages/openapi3/src/types.ts
+++ b/packages/openapi3/src/types.ts
@@ -618,6 +618,11 @@ export type OpenAPI3ParameterBase = Extensions & {
explode?: boolean;
schema: OpenAPI3Schema;
+
+ /** A free-form property to include an example of an instance for this schema. To represent examples that cannot be naturally represented in JSON or YAML, a string value can be used to contain the example with escaping where necessary. */
+ example?: any;
+
+ examples?: Record>;
};
export type OpenAPI3QueryParameter = OpenAPI3ParameterBase & {
diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts
index 90bb5ddbde6..36c4b52ee59 100644
--- a/packages/openapi3/src/util.ts
+++ b/packages/openapi3/src/util.ts
@@ -13,7 +13,7 @@ import {
Type,
Value,
} from "@typespec/compiler";
-import { HttpOperation } from "@typespec/http";
+import { HttpOperation, HttpProperty } from "@typespec/http";
import { createDiagnostic } from "./lib.js";
/**
* Checks if two objects are deeply equal.
@@ -189,3 +189,14 @@ function reportInvalidKey(program: Program, type: Type, key: string) {
function createValidKey(invalidKey: string): string {
return invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, "_");
}
+
+export type HttpParameterProperties = Extract<
+ HttpProperty,
+ { kind: "header" | "query" | "path" | "cookie" }
+>;
+
+export function isHttpParameterProperty(
+ httpProperty: HttpProperty,
+): httpProperty is HttpParameterProperties {
+ return ["header", "query", "path", "cookie"].includes(httpProperty.kind);
+}
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 34649f87b0c..76de4dc7ff7 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { OpenAPI3Document, OpenAPI3RequestBody } from "../src/types.js";
+import { OpenAPI3Document, OpenAPI3Parameter, OpenAPI3RequestBody } from "../src/types.js";
import { openApiFor } from "./test-host.js";
import { worksFor } from "./works-for.js";
@@ -234,4 +234,384 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
age: 2,
});
});
+
+ describe("parameters", () => {
+ it.each([
+ {
+ desc: "form (undefined)",
+ param: `@query color: string | null`,
+ paramExample: `null`,
+ expectedExample: "?color=",
+ },
+ {
+ desc: "form (string)",
+ param: `@query color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "?color=blue",
+ },
+ {
+ desc: "form (array) explode: false",
+ param: `@query color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue,black,brown",
+ },
+ {
+ desc: "form (array) explode: true",
+ param: `@query(#{ explode: true }) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue&color=black&color=brown",
+ },
+ {
+ desc: "form (object) explode: false",
+ param: `@query color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?color=R,100,G,200,B,150",
+ },
+ {
+ desc: "form (object) explode: true",
+ param: `@query(#{ explode: true }) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?R=100&G=200&B=150",
+ },
+ {
+ desc: "spaceDelimited (undefined)",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: string | null`,
+ paramExample: `null`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "spaceDelimited (string)",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: string`,
+ paramExample: `"blue"`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "spaceDelimited (array) explode: false",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue%20black%20brown",
+ },
+ {
+ desc: "spaceDelimited (array) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.spaceDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "spaceDelimited (object) explode: false",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?color=R%20100%20G%20200%20B%20150",
+ },
+ {
+ desc: "spaceDelimited (object) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.spaceDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (undefined)",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: string | null`,
+ paramExample: `null`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (string)",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: string`,
+ paramExample: `"blue"`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (array) explode: false",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue%7Cblack%7Cbrown",
+ },
+ {
+ desc: "pipeDelimited (array) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.pipeDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (object) explode: false",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?color=R%7C100%7CG%7C200%7CB%7C150",
+ },
+ {
+ desc: "pipeDelimited (object) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.pipeDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: undefined,
+ },
+ ])(
+ "set example on the query parameter with style $desc",
+ async ({ param, paramExample, expectedExample }) => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ },
+ );
+
+ it.each([
+ {
+ desc: "simple (undefined)",
+ route: "/{color}",
+ param: `@path color: string | null`,
+ paramExample: `null`,
+ expectedExample: "",
+ },
+ {
+ desc: "simple (string)",
+ route: "/{color}",
+ param: `@path color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "blue",
+ },
+ {
+ desc: "simple (array) explode: false",
+ route: "/{color}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (array) explode: true",
+ route: "/{color*}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (object) explode: false",
+ route: "/{color}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R,100,G,200,B,150",
+ },
+ {
+ desc: "simple (object) explode: true",
+ route: "/{color*}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R=100,G=200,B=150",
+ },
+ {
+ desc: "matrix (undefined)",
+ route: "/{;color}",
+ param: `@path color: string | null`,
+ paramExample: `null`,
+ expectedExample: ";color",
+ },
+ {
+ desc: "matrix (string)",
+ route: "/{;color}",
+ param: `@path color: string`,
+ paramExample: `"blue"`,
+ expectedExample: ";color=blue",
+ },
+ {
+ desc: "matrix (array) explode: false",
+ route: "/{;color}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ";color=blue,black,brown",
+ },
+ {
+ desc: "matrix (array) explode: true",
+ route: "/{;color*}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ";color=blue;color=black;color=brown",
+ },
+ {
+ desc: "matrix (object) explode: false",
+ route: "/{;color}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ";color=R,100,G,200,B,150",
+ },
+ {
+ desc: "matrix (object) explode: true",
+ route: "/{;color*}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ";R=100;G=200;B=150",
+ },
+ {
+ desc: "label (undefined)",
+ route: "/{.color}",
+ param: `@path color: string | null`,
+ paramExample: `null`,
+ expectedExample: ".",
+ },
+ {
+ desc: "label (string)",
+ route: "/{.color}",
+ param: `@path color: string`,
+ paramExample: `"blue"`,
+ expectedExample: ".blue",
+ },
+ {
+ desc: "label (array) explode: false",
+ route: "/{.color}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ".blue,black,brown",
+ },
+ {
+ desc: "label (array) explode: true",
+ route: "/{.color*}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ".blue.black.brown",
+ },
+ {
+ desc: "label (object) explode: false",
+ route: "/{.color}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ".R,100,G,200,B,150",
+ },
+ {
+ desc: "label (object) explode: true",
+ route: "/{.color*}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ".R=100.G=200.B=150",
+ },
+ ])(
+ "set example on the path parameter with style $desc",
+ async ({ param, route, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("${route}")
+ op getColors(${param}): void;
+ `,
+ );
+ expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ },
+ );
+
+ it.each([
+ {
+ desc: "simple (undefined)",
+ param: `@header color: string | null`,
+ paramExample: `null`,
+ expectedExample: "",
+ },
+ {
+ desc: "simple (string)",
+ param: `@header color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "blue",
+ },
+ {
+ desc: "simple (array) explode: false",
+ param: `@header color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (array) explode: true",
+ param: `@header(#{ explode: true }) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (object) explode: false",
+ param: `@header color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R,100,G,200,B,150",
+ },
+ {
+ desc: "simple (object) explode: true",
+ param: `@header(#{ explode: true }) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R=100,G=200,B=150",
+ },
+ ])(
+ "set example on the header parameter with style $desc",
+ async ({ param, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ },
+ );
+
+ it.each([
+ {
+ desc: "form (undefined)",
+ param: `@cookie color: string | null`,
+ paramExample: `null`,
+ expectedExample: "color=",
+ },
+ {
+ desc: "form (string)",
+ param: `@cookie color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "color=blue",
+ },
+ {
+ desc: "form (array) explode: false",
+ param: `@cookie color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "color=blue,black,brown",
+ },
+ {
+ desc: "form (object) explode: false",
+ param: `@cookie color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "color=R,100,G,200,B,150",
+ },
+ ])(
+ "set example on the cookie parameter with style $desc",
+ async ({ param, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ },
+ );
+ });
});
From 5a7f895152f89fe3cfca37be9f39f3dfbc88c9f8 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 20 May 2025 12:46:02 -0700
Subject: [PATCH 02/14] add changelog
---
.../changes/oa3-examples-on-params-2025-4-20-12-45-54.md | 7 +++++++
1 file changed, 7 insertions(+)
create mode 100644 .chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md
diff --git a/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md b/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md
new file mode 100644
index 00000000000..7a9ed849dfd
--- /dev/null
+++ b/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+ - "@typespec/openapi3"
+---
+
+Adds support for parameter examples via `@opExample`
\ No newline at end of file
From ed5265528d67ed891f1bcfbdf3185a39f4f52317 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 20 May 2025 12:56:43 -0700
Subject: [PATCH 03/14] make cspell happy
---
packages/openapi3/src/examples.ts | 1 +
packages/openapi3/test/examples.test.ts | 1 +
2 files changed, 2 insertions(+)
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 1499c1fe023..4678eaa33f4 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -464,6 +464,7 @@ function getParameterDelimitedValue(
if (explode) return undefined;
const tk = $(program);
+ // cspell: ignore Cblack Cbrown
/*
https://spec.openapis.org/oas/v3.0.4.html#style-examples
array -> ["blue", "black", "brown"]
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 76de4dc7ff7..6c08808583f 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -325,6 +325,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
desc: "pipeDelimited (array) explode: false",
param: `@query @encode(ArrayEncoding.pipeDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
+ // cspell:disable-next-line
expectedExample: "?color=blue%7Cblack%7Cbrown",
},
{
From ccce21695882d6bab0f643b4791ae8c8c7ec024d Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 20 May 2025 15:24:49 -0700
Subject: [PATCH 04/14] default to no parameter example serialization
---
packages/openapi3/README.md | 8 ++
packages/openapi3/src/examples.ts | 17 ++-
packages/openapi3/src/lib.ts | 17 +++
packages/openapi3/src/openapi.ts | 10 +-
packages/openapi3/test/examples.test.ts | 130 ++++++++++++------
.../emitters/openapi3/reference/emitter.md | 8 ++
6 files changed, 145 insertions(+), 45 deletions(-)
diff --git a/packages/openapi3/README.md b/packages/openapi3/README.md
index f650ab8a3f2..779e23868ea 100644
--- a/packages/openapi3/README.md
+++ b/packages/openapi3/README.md
@@ -125,6 +125,14 @@ If true, then for models emitted as object schemas we default `additionalPropert
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
+### `serialize-parameter-examples`
+
+**Type:** `boolean`
+
+If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
+Default: `false`
+See https://spec.openapis.org/oas/v3.0.4.html#style-examples
+
## Decorators
### TypeSpec.OpenAPI
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 4678eaa33f4..14244765d13 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -35,9 +35,14 @@ export interface OperationExamples {
responses: Record>;
}
+type ResolveOperationExamplesOptions = {
+ enableParameterSerialization: boolean;
+};
+
export function resolveOperationExamples(
program: Program,
operation: HttpOperation | SharedHttpOperation,
+ { enableParameterSerialization }: ResolveOperationExamplesOptions,
): OperationExamples {
const examples = findOperationExamples(program, operation);
const result: OperationExamples = { requestBody: {}, parameters: {}, responses: {} };
@@ -67,7 +72,12 @@ export function resolveOperationExamples(
for (const property of op.parameters.properties) {
if (!isHttpParameterProperty(property)) continue;
- const value = getParameterValue(program, example.parameters, property);
+ const value = getParameterValue(
+ program,
+ example.parameters,
+ property,
+ enableParameterSerialization,
+ );
if (value) {
const parameterName = property.options.name;
result.parameters[parameterName] ??= [];
@@ -268,8 +278,13 @@ function getParameterValue(
program: Program,
parameterExamples: Value,
property: HttpParameterProperties,
+ enableSerialization: boolean = false,
): Value | undefined {
const value = getValueByPath(parameterExamples, property.path);
+ if (!enableSerialization) {
+ return value;
+ }
+
if (!value) return value;
// Depending on the parameter type, we may need to serialize the value differently.
diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts
index cbf01f1ae29..ebedd31262b 100644
--- a/packages/openapi3/src/lib.ts
+++ b/packages/openapi3/src/lib.ts
@@ -81,6 +81,13 @@ export interface OpenAPI3EmitterOptions {
* @default false
*/
"seal-object-schemas"?: boolean;
+
+ /**
+ * If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
+ * @default false
+ * @see https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ */
+ "serialize-parameter-examples"?: boolean;
}
const EmitterOptionsSchema: JSONSchemaType = {
@@ -180,6 +187,16 @@ const EmitterOptionsSchema: JSONSchemaType = {
"Default: `false`",
].join("\n"),
},
+ "serialize-parameter-examples": {
+ type: "boolean",
+ nullable: true,
+ default: false,
+ description: [
+ "If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.",
+ "Default: `false`",
+ "See https://spec.openapis.org/oas/v3.0.4.html#style-examples",
+ ].join("\n"),
+ },
},
required: [],
};
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index 74d22b16a23..dfee294b504 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -211,6 +211,7 @@ export function resolveOptions(
outputFile: resolvePath(context.emitterOutputDir, specDir, outputFile),
openapiVersions,
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
+ serializeParameterExamples: resolvedOptions["serialize-parameter-examples"] ?? false,
};
}
@@ -223,6 +224,7 @@ export interface ResolvedOpenAPI3EmitterOptions {
includeXTypeSpecName: "inline-only" | "never";
safeintStrategy: "double-int" | "int64";
sealObjectSchemas: boolean;
+ serializeParameterExamples: boolean;
}
function createOAPIEmitter(
@@ -732,7 +734,9 @@ function createOAPIEmitter(
const operations = shared.operations;
const verb = operations[0].verb;
const path = operations[0].path;
- const examples = resolveOperationExamples(program, shared);
+ const examples = resolveOperationExamples(program, shared, {
+ enableParameterSerialization: options.serializeParameterExamples,
+ });
const oai3Operation: OpenAPI3Operation = {
operationId: computeSharedOperationId(shared),
parameters: [],
@@ -810,7 +814,9 @@ function createOAPIEmitter(
return undefined;
}
const visibility = resolveRequestVisibility(program, operation.operation, verb);
- const examples = resolveOperationExamples(program, operation);
+ const examples = resolveOperationExamples(program, operation, {
+ enableParameterSerialization: options.serializeParameterExamples,
+ });
const oai3Operation: OpenAPI3Operation = {
operationId: resolveOperationId(program, operation.operation),
summary: getSummary(program, operation.operation),
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 6c08808583f..fe608c999ff 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -235,7 +235,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
- describe("parameters", () => {
+ describe("set example on the query parameter with serialization enabled", () => {
it.each([
{
desc: "form (undefined)",
@@ -346,11 +346,9 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: undefined,
},
- ])(
- "set example on the query parameter with style $desc",
- async ({ param, paramExample, expectedExample }) => {
- const res = await openApiFor(
- `
+ ])("$desc", async ({ param, paramExample, expectedExample }) => {
+ const res = await openApiFor(
+ `
@opExample(#{
parameters: #{
color: ${paramExample},
@@ -359,13 +357,60 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@route("/")
op getColors(${param}): void;
`,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+
+ describe.each(["path", "query", "header", "cookie"])(
+ "set example on the %s parameter without serialization",
+ (paramType) => {
+ it.each([
+ {
+ param: `@${paramType} color: string | null`,
+ paramExample: `null`,
+ expectedExample: null,
+ },
+ {
+ param: `@${paramType} color: string | null`,
+ paramExample: `"blue"`,
+ expectedExample: "blue",
+ },
+ {
+ param: `@${paramType} color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ["blue", "black", "brown"],
+ },
+ {
+ param: `@${paramType} color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: { R: 100, G: 200, B: 150 },
+ },
+ ])("$paramExample", async ({ param, paramExample, expectedExample }) => {
+ const path = paramType === "path" ? "/{color}" : "/";
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
);
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expect((res.paths[path].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
expectedExample,
);
- },
- );
+ });
+ },
+ );
+ describe("set example on the path parameter", () => {
it.each([
{
desc: "simple (undefined)",
@@ -493,11 +538,9 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: ".R=100.G=200.B=150",
},
- ])(
- "set example on the path parameter with style $desc",
- async ({ param, route, paramExample, expectedExample }) => {
- const res: OpenAPI3Document = await openApiFor(
- `
+ ])("$desc", async ({ param, route, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
@opExample(#{
parameters: #{
color: ${paramExample},
@@ -506,13 +549,16 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@route("${route}")
op getColors(${param}): void;
`,
- );
- expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- },
- );
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+ describe("set example on the header parameter", () => {
it.each([
{
desc: "simple (undefined)",
@@ -550,11 +596,9 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: "R=100,G=200,B=150",
},
- ])(
- "set example on the header parameter with style $desc",
- async ({ param, paramExample, expectedExample }) => {
- const res: OpenAPI3Document = await openApiFor(
- `
+ ])("$desc", async ({ param, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
@opExample(#{
parameters: #{
color: ${paramExample},
@@ -563,13 +607,16 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@route("/")
op getColors(${param}): void;
`,
- );
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- },
- );
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+ describe("set example on the cookie parameter", () => {
it.each([
{
desc: "form (undefined)",
@@ -595,11 +642,9 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
paramExample: `#{R: 100, G: 200, B: 150}`,
expectedExample: "color=R,100,G,200,B,150",
},
- ])(
- "set example on the cookie parameter with style $desc",
- async ({ param, paramExample, expectedExample }) => {
- const res: OpenAPI3Document = await openApiFor(
- `
+ ])("$desc", async ({ param, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
@opExample(#{
parameters: #{
color: ${paramExample},
@@ -608,11 +653,12 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@route("/")
op getColors(${param}): void;
`,
- );
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- },
- );
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
});
});
diff --git a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
index 66b71c12c7d..bc437b1743e 100644
--- a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
+++ b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
@@ -118,3 +118,11 @@ Default: `int64`
If true, then for models emitted as object schemas we default `additionalProperties` to false for
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
+
+### `serialize-parameter-examples`
+
+**Type:** `boolean`
+
+If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
+Default: `false`
+See https://spec.openapis.org/oas/v3.0.4.html#style-examples
From 8653ed7e28b6f03a75f59b17fc2633a90f51e779 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 20 May 2025 15:36:23 -0700
Subject: [PATCH 05/14] test with multiple examples per param
---
packages/openapi3/test/examples.test.ts | 154 ++++++++++++++++--------
1 file changed, 107 insertions(+), 47 deletions(-)
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index fe608c999ff..572b25876f7 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -235,6 +235,50 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
+ describe.each(["path", "query", "header", "cookie"])(
+ "set example on the %s parameter without serialization",
+ (paramType) => {
+ it.each([
+ {
+ param: `@${paramType} color: string | null`,
+ paramExample: `null`,
+ expectedExample: null,
+ },
+ {
+ param: `@${paramType} color: string | null`,
+ paramExample: `"blue"`,
+ expectedExample: "blue",
+ },
+ {
+ param: `@${paramType} color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ["blue", "black", "brown"],
+ },
+ {
+ param: `@${paramType} color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: { R: 100, G: 200, B: 150 },
+ },
+ ])("$paramExample", async ({ param, paramExample, expectedExample }) => {
+ const path = paramType === "path" ? "/{color}" : "/";
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ );
+ expect((res.paths[path].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ },
+ );
+
describe("set example on the query parameter with serialization enabled", () => {
it.each([
{
@@ -366,51 +410,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
- describe.each(["path", "query", "header", "cookie"])(
- "set example on the %s parameter without serialization",
- (paramType) => {
- it.each([
- {
- param: `@${paramType} color: string | null`,
- paramExample: `null`,
- expectedExample: null,
- },
- {
- param: `@${paramType} color: string | null`,
- paramExample: `"blue"`,
- expectedExample: "blue",
- },
- {
- param: `@${paramType} color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: ["blue", "black", "brown"],
- },
- {
- param: `@${paramType} color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: { R: 100, G: 200, B: 150 },
- },
- ])("$paramExample", async ({ param, paramExample, expectedExample }) => {
- const path = paramType === "path" ? "/{color}" : "/";
- const res = await openApiFor(
- `
- @opExample(#{
- parameters: #{
- color: ${paramExample},
- },
- })
- @route("/")
- op getColors(${param}): void;
- `,
- );
- expect((res.paths[path].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- });
- },
- );
-
- describe("set example on the path parameter", () => {
+ describe("set example on the path parameter with serialization enabled", () => {
it.each([
{
desc: "simple (undefined)",
@@ -558,7 +558,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
- describe("set example on the header parameter", () => {
+ describe("set example on the header parameter with serialization enabled", () => {
it.each([
{
desc: "simple (undefined)",
@@ -616,7 +616,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
- describe("set example on the cookie parameter", () => {
+ describe("set example on the cookie parameter with serialization enabled", () => {
it.each([
{
desc: "form (undefined)",
@@ -661,4 +661,64 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
);
});
});
+
+ it("supports multiple examples on parameter with serialization enabled", async () => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: "green",
+ },
+ }, #{ title: "MyExample" })
+ @opExample(#{
+ parameters: #{
+ color: "red",
+ },
+ }, #{ title: "MyExample2" })
+ @route("/")
+ op getColors(@query color: string): void;
+ `,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
+ MyExample: {
+ summary: "MyExample",
+ value: "?color=green",
+ },
+ MyExample2: {
+ summary: "MyExample2",
+ value: "?color=red",
+ },
+ });
+ });
+
+ it("supports multiple examples on parameter without serialization enabled", async () => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: "green",
+ },
+ }, #{ title: "MyExample" })
+ @opExample(#{
+ parameters: #{
+ color: "red",
+ },
+ }, #{ title: "MyExample2" })
+ @route("/")
+ op getColors(@query color: string): void;
+ `,
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
+ MyExample: {
+ summary: "MyExample",
+ value: "green",
+ },
+ MyExample2: {
+ summary: "MyExample2",
+ value: "red",
+ },
+ });
+ });
});
From 30a082decf8d0664cf68fda64b6edc21276613f6 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Thu, 22 May 2025 11:47:25 -0700
Subject: [PATCH 06/14] remove parameter example serialization
---
packages/openapi3/src/examples.ts | 360 +--------------------
packages/openapi3/src/lib.ts | 17 -
packages/openapi3/src/openapi.ts | 10 +-
packages/openapi3/test/examples.test.ts | 414 ------------------------
4 files changed, 3 insertions(+), 798 deletions(-)
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 14244765d13..86fe4510a44 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -1,17 +1,13 @@
import {
- BooleanValue,
Example,
getOpExamples,
ignoreDiagnostics,
- NumericValue,
OpExample,
Program,
serializeValueAsJson,
- StringValue,
Type,
Value,
} from "@typespec/compiler";
-import { $ } from "@typespec/compiler/typekit";
import type {
HttpOperation,
HttpOperationResponse,
@@ -19,7 +15,6 @@ import type {
HttpProperty,
HttpStatusCodeRange,
} from "@typespec/http";
-import { getParameterStyle } from "./parameters.js";
import { getOpenAPI3StatusCodes } from "./status-codes.js";
import { OpenAPI3Example, OpenAPI3MediaType } from "./types.js";
import {
@@ -35,14 +30,9 @@ export interface OperationExamples {
responses: Record>;
}
-type ResolveOperationExamplesOptions = {
- enableParameterSerialization: boolean;
-};
-
export function resolveOperationExamples(
program: Program,
operation: HttpOperation | SharedHttpOperation,
- { enableParameterSerialization }: ResolveOperationExamplesOptions,
): OperationExamples {
const examples = findOperationExamples(program, operation);
const result: OperationExamples = { requestBody: {}, parameters: {}, responses: {} };
@@ -72,12 +62,7 @@ export function resolveOperationExamples(
for (const property of op.parameters.properties) {
if (!isHttpParameterProperty(property)) continue;
- const value = getParameterValue(
- program,
- example.parameters,
- property,
- enableParameterSerialization,
- );
+ const value = getParameterValue(program, example.parameters, property);
if (value) {
const parameterName = property.options.name;
result.parameters[parameterName] ??= [];
@@ -278,354 +263,11 @@ function getParameterValue(
program: Program,
parameterExamples: Value,
property: HttpParameterProperties,
- enableSerialization: boolean = false,
): Value | undefined {
const value = getValueByPath(parameterExamples, property.path);
- if (!enableSerialization) {
- return value;
- }
-
- if (!value) return value;
-
- // Depending on the parameter type, we may need to serialize the value differently.
- // https://spec.openapis.org/oas/v3.0.4.html#style-examples
- /*
- Supported styles per location: https://spec.openapis.org/oas/v3.0.4.html#style-values
- | Location | Default Style | Supported Styles |
- | -------- | ------------- | ----------------------------------------------- |
- | query | form | form, spaceDelimited, pipeDelimited, deepObject |
- | header | simple | simple |
- | path | simple | simple, label, matrix |
- | cookie | form | form |
- */
- // explode is only relevant for array/object types
-
- if (property.kind === "query") {
- return getQueryParameterValue(program, value, property);
- } else if (property.kind === "header") {
- return getHeaderParameterValue(program, value, property);
- } else if (property.kind === "path") {
- return getPathParameterValue(program, value, property);
- } else if (property.kind === "cookie") {
- return getCookieParameterValue(program, value, property);
- }
-
return value;
}
-function getQueryParameterValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- const style = getParameterStyle(program, property.property) ?? "form";
-
- switch (style) {
- case "form":
- return getParameterFormValue(program, originalValue, property);
- case "spaceDelimited":
- return getParameterDelimitedValue(program, originalValue, property, " ");
- case "pipeDelimited":
- return getParameterDelimitedValue(program, originalValue, property, "|");
- }
-}
-
-function getHeaderParameterValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- return getParameterSimpleValue(program, originalValue, property);
-}
-
-function getPathParameterValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- const { style } = property.options;
-
- if (style === "label") {
- return getParameterLabelValue(program, originalValue, property);
- } else if (style === "matrix") {
- return getParameterMatrixValue(program, originalValue, property);
- } else if (style === "simple") {
- return getParameterSimpleValue(program, originalValue, property);
- }
-
- return undefined;
-}
-
-function getCookieParameterValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- return getParameterFormValue(program, originalValue, property);
-}
-
-function getParameterLabelValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- const { explode } = property.options;
- const tk = $(program);
-
- /*
- https://spec.openapis.org/oas/v3.0.4.html#style-examples
- string -> "blue"
- array -> ["blue", "black", "brown"]
- object -> { "R": 100, "G": 200, "B": 150 }
-
- | explode | string | array | object |
- | ------- | ------- | ----------------- | ------------------ |
- | false | .blue | .blue,black,brown | .R,100,G,200,B,150 |
- | true | .blue | .blue.black.brown | .R=100.G=200.B=150 |
- */
-
- const joiner = explode ? "." : ",";
- if (tk.value.isArray(originalValue)) {
- const pairs: string[] = [];
- for (const value of originalValue.values) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(`${value.value}`);
- }
- return tk.value.createString(`.${pairs.join(joiner)}`);
- }
-
- if (tk.value.isObject(originalValue)) {
- const pairs: string[] = [];
- for (const [key, { value }] of originalValue.properties) {
- if (!isSerializableScalarValue(value)) continue;
- const sep = explode ? "=" : ",";
- pairs.push(`${key}${sep}${value.value}`);
- }
- return tk.value.createString(`.${pairs.join(joiner)}`);
- }
-
- // null (undefined) is treated as a a dot
- if (tk.value.isNull(originalValue)) {
- return tk.value.createString(".");
- }
-
- if (isSerializableScalarValue(originalValue)) {
- return tk.value.createString(`.${originalValue.value}`);
- }
-
- return;
-}
-
-function getParameterMatrixValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- const { explode, name } = property.options;
- const tk = $(program);
-
- /*
- https://spec.openapis.org/oas/v3.0.4.html#style-examples
- string -> "blue"
- array -> ["blue", "black", "brown"]
- object -> { "R": 100, "G": 200, "B": 150 }
-
- | explode | string | array | object |
- | ------- | ------------- | ----------------------------------- | ------------------------ |
- | false | ;color=blue | ;color=blue,black,brown | ;color=R,100,G,200,B,150 |
- | true | ;color=blue | ;color=blue;color=black;color=brown | ;R=100;G=200;B=150 |
- */
-
- const joiner = explode ? ";" : ",";
- const prefix = explode ? "" : `${name}=`;
- if (tk.value.isArray(originalValue)) {
- const pairs: string[] = [];
- for (const value of originalValue.values) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
- }
- return tk.value.createString(`;${prefix}${pairs.join(joiner)}`);
- }
-
- if (tk.value.isObject(originalValue)) {
- const sep = explode ? "=" : ",";
- const pairs: string[] = [];
- for (const [key, { value }] of originalValue.properties) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(`${key}${sep}${value.value}`);
- }
- return tk.value.createString(`;${prefix}${pairs.join(joiner)}`);
- }
-
- if (tk.value.isNull(originalValue)) {
- return tk.value.createString(`;${name}`);
- }
-
- if (isSerializableScalarValue(originalValue)) {
- return tk.value.createString(`;${name}=${originalValue.value}`);
- }
-
- return;
-}
-
-function getParameterDelimitedValue(
- program: Program,
- originalValue: Value,
- property: Extract,
- delimiter: " " | "|",
-): Value | undefined {
- const { explode, name } = property.options;
- // Serialization is undefined for explode=true
- if (explode) return undefined;
-
- const tk = $(program);
- // cspell: ignore Cblack Cbrown
- /*
- https://spec.openapis.org/oas/v3.0.4.html#style-examples
- array -> ["blue", "black", "brown"]
- object -> { "R": 100, "G": 200, "B": 150 }
-
- | style | explode | string | array | object |
- | ----- | ------- | ------ | --------------------------- | ---------------------------------- |
- | pipe | false | n/a | ?color=blue%7Cblack%7Cbrown | ?color=R%7C100%7CG%7C200%7CB%7C150 |
- | pipe | true | n/a | n/a | n/a |
- | space | false | n/a | ?color=blue%20black%20brown | ?color=R%20100%20G%20200%20B%20150 |
- | space | true | n/a | n/a | n/a |
- */
-
- if (tk.value.isArray(originalValue)) {
- const pairs: string[] = [];
- for (const value of originalValue.values) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(`${value.value}`);
- }
- return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
- }
-
- if (tk.value.isObject(originalValue)) {
- const pairs: string[] = [];
- for (const [key, { value }] of originalValue.properties) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(`${key}${delimiter}${value.value}`);
- }
- return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
- }
-
- return undefined;
-}
-
-function getParameterFormValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- const { name } = property.options;
- const isCookie = property.kind === "cookie";
- const explode = isCookie ? false : property.options.explode;
- const tk = $(program);
- /*
- https://spec.openapis.org/oas/v3.0.4.html#style-examples
- string -> "blue"
- array -> ["blue", "black", "brown"]
- object -> { "R": 100, "G": 200, "B": 150 }
-
- | explode | string | array | object |
- | ------- | ------------- | ----------------------------------- | ------------------------ |
- | false | ?color=blue | ?color=blue,black,brown | ?color=R,100,G,200,B,150 |
- | true | ?color=blue | ?color=blue&color=black&color=brown | ?R=100&G=200&B=150 |
- */
-
- const qPrefix = isCookie ? "" : "?";
- const prefix = explode ? "" : `${name}=`;
- if (tk.value.isArray(originalValue)) {
- const sep = explode ? "&" : ",";
- const pairs: string[] = [];
- for (const value of originalValue.values) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
- }
- return tk.value.createString(`${qPrefix}${prefix}${pairs.join(sep)}`);
- }
-
- if (tk.value.isObject(originalValue)) {
- const sep = explode ? "=" : ",";
- const joiner = explode ? "&" : ",";
- const pairs: string[] = [];
- for (const [key, { value }] of originalValue.properties) {
- if (!isSerializableScalarValue(value)) continue;
- pairs.push(`${key}${sep}${value.value}`);
- }
- return tk.value.createString(`${qPrefix}${prefix}${pairs.join(joiner)}`);
- }
-
- if (isSerializableScalarValue(originalValue)) {
- return tk.value.createString(`${qPrefix}${name}=${originalValue.value}`);
- }
-
- // null is treated as the 'undefined' value
- if (tk.value.isNull(originalValue)) {
- return tk.value.createString(`${qPrefix}${name}=`);
- }
-
- return;
-}
-
-function getParameterSimpleValue(
- program: Program,
- originalValue: Value,
- property: Extract,
-): Value | undefined {
- const { explode } = property.options;
- const tk = $(program);
-
- /*
- https://spec.openapis.org/oas/v3.0.4.html#style-examples
- string -> "blue"
- array -> ["blue", "black", "brown"]
- object -> { "R": 100, "G": 200, "B": 150 }
-
- | explode | string | array | object |
- | ------- | ------ | ---------------- | ----------------- |
- | false | blue | blue,black,brown | R,100,G,200,B,150 |
- | true | blue | blue,black,brown | R=100,G=200,B=150 |
- */
-
- if (tk.value.isArray(originalValue)) {
- const serializedValue = originalValue.values
- .filter(isSerializableScalarValue)
- .map((v) => v.value)
- .join(",");
- return tk.value.createString(serializedValue);
- }
-
- if (tk.value.isObject(originalValue)) {
- const pairs: string[] = [];
- for (const [key, { value }] of originalValue.properties) {
- if (!isSerializableScalarValue(value)) continue;
- const sep = explode ? "=" : ",";
- pairs.push(`${key}${sep}${value.value}`);
- }
- return tk.value.createString(pairs.join(","));
- }
-
- // null (undefined) is treated as an empty string - unrelated to allowEmptyValue
- if (tk.value.isNull(originalValue)) {
- return tk.value.createString("");
- }
-
- if (isSerializableScalarValue(originalValue)) {
- return originalValue;
- }
-
- return;
-}
-
-function isSerializableScalarValue(
- value: Value,
-): value is BooleanValue | NumericValue | StringValue {
- return ["BooleanValue", "NumericValue", "StringValue"].includes(value.valueKind);
-}
-
function getValueByPath(value: Value, path: (string | number)[]): Value | undefined {
let current: Value | undefined = value;
for (const key of path) {
diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts
index ebedd31262b..cbf01f1ae29 100644
--- a/packages/openapi3/src/lib.ts
+++ b/packages/openapi3/src/lib.ts
@@ -81,13 +81,6 @@ export interface OpenAPI3EmitterOptions {
* @default false
*/
"seal-object-schemas"?: boolean;
-
- /**
- * If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
- * @default false
- * @see https://spec.openapis.org/oas/v3.0.4.html#style-examples
- */
- "serialize-parameter-examples"?: boolean;
}
const EmitterOptionsSchema: JSONSchemaType = {
@@ -187,16 +180,6 @@ const EmitterOptionsSchema: JSONSchemaType = {
"Default: `false`",
].join("\n"),
},
- "serialize-parameter-examples": {
- type: "boolean",
- nullable: true,
- default: false,
- description: [
- "If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.",
- "Default: `false`",
- "See https://spec.openapis.org/oas/v3.0.4.html#style-examples",
- ].join("\n"),
- },
},
required: [],
};
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index dfee294b504..74d22b16a23 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -211,7 +211,6 @@ export function resolveOptions(
outputFile: resolvePath(context.emitterOutputDir, specDir, outputFile),
openapiVersions,
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
- serializeParameterExamples: resolvedOptions["serialize-parameter-examples"] ?? false,
};
}
@@ -224,7 +223,6 @@ export interface ResolvedOpenAPI3EmitterOptions {
includeXTypeSpecName: "inline-only" | "never";
safeintStrategy: "double-int" | "int64";
sealObjectSchemas: boolean;
- serializeParameterExamples: boolean;
}
function createOAPIEmitter(
@@ -734,9 +732,7 @@ function createOAPIEmitter(
const operations = shared.operations;
const verb = operations[0].verb;
const path = operations[0].path;
- const examples = resolveOperationExamples(program, shared, {
- enableParameterSerialization: options.serializeParameterExamples,
- });
+ const examples = resolveOperationExamples(program, shared);
const oai3Operation: OpenAPI3Operation = {
operationId: computeSharedOperationId(shared),
parameters: [],
@@ -814,9 +810,7 @@ function createOAPIEmitter(
return undefined;
}
const visibility = resolveRequestVisibility(program, operation.operation, verb);
- const examples = resolveOperationExamples(program, operation, {
- enableParameterSerialization: options.serializeParameterExamples,
- });
+ const examples = resolveOperationExamples(program, operation);
const oai3Operation: OpenAPI3Operation = {
operationId: resolveOperationId(program, operation.operation),
summary: getSummary(program, operation.operation),
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 572b25876f7..74a57f16b3e 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -279,420 +279,6 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
},
);
- describe("set example on the query parameter with serialization enabled", () => {
- it.each([
- {
- desc: "form (undefined)",
- param: `@query color: string | null`,
- paramExample: `null`,
- expectedExample: "?color=",
- },
- {
- desc: "form (string)",
- param: `@query color: string`,
- paramExample: `"blue"`,
- expectedExample: "?color=blue",
- },
- {
- desc: "form (array) explode: false",
- param: `@query color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "?color=blue,black,brown",
- },
- {
- desc: "form (array) explode: true",
- param: `@query(#{ explode: true }) color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "?color=blue&color=black&color=brown",
- },
- {
- desc: "form (object) explode: false",
- param: `@query color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?color=R,100,G,200,B,150",
- },
- {
- desc: "form (object) explode: true",
- param: `@query(#{ explode: true }) color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?R=100&G=200&B=150",
- },
- {
- desc: "spaceDelimited (undefined)",
- param: `@query @encode(ArrayEncoding.spaceDelimited) color: string | null`,
- paramExample: `null`,
- expectedExample: undefined,
- },
- {
- desc: "spaceDelimited (string)",
- param: `@query @encode(ArrayEncoding.spaceDelimited) color: string`,
- paramExample: `"blue"`,
- expectedExample: undefined,
- },
- {
- desc: "spaceDelimited (array) explode: false",
- param: `@query @encode(ArrayEncoding.spaceDelimited) color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "?color=blue%20black%20brown",
- },
- {
- desc: "spaceDelimited (array) explode: true",
- param: `@query(#{ explode: true }) @encode(ArrayEncoding.spaceDelimited) color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: undefined,
- },
- {
- desc: "spaceDelimited (object) explode: false",
- param: `@query @encode(ArrayEncoding.spaceDelimited) color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?color=R%20100%20G%20200%20B%20150",
- },
- {
- desc: "spaceDelimited (object) explode: true",
- param: `@query(#{ explode: true }) @encode(ArrayEncoding.spaceDelimited) color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: undefined,
- },
- {
- desc: "pipeDelimited (undefined)",
- param: `@query @encode(ArrayEncoding.pipeDelimited) color: string | null`,
- paramExample: `null`,
- expectedExample: undefined,
- },
- {
- desc: "pipeDelimited (string)",
- param: `@query @encode(ArrayEncoding.pipeDelimited) color: string`,
- paramExample: `"blue"`,
- expectedExample: undefined,
- },
- {
- desc: "pipeDelimited (array) explode: false",
- param: `@query @encode(ArrayEncoding.pipeDelimited) color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- // cspell:disable-next-line
- expectedExample: "?color=blue%7Cblack%7Cbrown",
- },
- {
- desc: "pipeDelimited (array) explode: true",
- param: `@query(#{ explode: true }) @encode(ArrayEncoding.pipeDelimited) color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: undefined,
- },
- {
- desc: "pipeDelimited (object) explode: false",
- param: `@query @encode(ArrayEncoding.pipeDelimited) color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?color=R%7C100%7CG%7C200%7CB%7C150",
- },
- {
- desc: "pipeDelimited (object) explode: true",
- param: `@query(#{ explode: true }) @encode(ArrayEncoding.pipeDelimited) color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: undefined,
- },
- ])("$desc", async ({ param, paramExample, expectedExample }) => {
- const res = await openApiFor(
- `
- @opExample(#{
- parameters: #{
- color: ${paramExample},
- },
- })
- @route("/")
- op getColors(${param}): void;
- `,
- undefined,
- { "serialize-parameter-examples": true },
- );
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- });
- });
-
- describe("set example on the path parameter with serialization enabled", () => {
- it.each([
- {
- desc: "simple (undefined)",
- route: "/{color}",
- param: `@path color: string | null`,
- paramExample: `null`,
- expectedExample: "",
- },
- {
- desc: "simple (string)",
- route: "/{color}",
- param: `@path color: string`,
- paramExample: `"blue"`,
- expectedExample: "blue",
- },
- {
- desc: "simple (array) explode: false",
- route: "/{color}",
- param: `@path color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "blue,black,brown",
- },
- {
- desc: "simple (array) explode: true",
- route: "/{color*}",
- param: `@path color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "blue,black,brown",
- },
- {
- desc: "simple (object) explode: false",
- route: "/{color}",
- param: `@path color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "R,100,G,200,B,150",
- },
- {
- desc: "simple (object) explode: true",
- route: "/{color*}",
- param: `@path color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "R=100,G=200,B=150",
- },
- {
- desc: "matrix (undefined)",
- route: "/{;color}",
- param: `@path color: string | null`,
- paramExample: `null`,
- expectedExample: ";color",
- },
- {
- desc: "matrix (string)",
- route: "/{;color}",
- param: `@path color: string`,
- paramExample: `"blue"`,
- expectedExample: ";color=blue",
- },
- {
- desc: "matrix (array) explode: false",
- route: "/{;color}",
- param: `@path color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: ";color=blue,black,brown",
- },
- {
- desc: "matrix (array) explode: true",
- route: "/{;color*}",
- param: `@path color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: ";color=blue;color=black;color=brown",
- },
- {
- desc: "matrix (object) explode: false",
- route: "/{;color}",
- param: `@path color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: ";color=R,100,G,200,B,150",
- },
- {
- desc: "matrix (object) explode: true",
- route: "/{;color*}",
- param: `@path color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: ";R=100;G=200;B=150",
- },
- {
- desc: "label (undefined)",
- route: "/{.color}",
- param: `@path color: string | null`,
- paramExample: `null`,
- expectedExample: ".",
- },
- {
- desc: "label (string)",
- route: "/{.color}",
- param: `@path color: string`,
- paramExample: `"blue"`,
- expectedExample: ".blue",
- },
- {
- desc: "label (array) explode: false",
- route: "/{.color}",
- param: `@path color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: ".blue,black,brown",
- },
- {
- desc: "label (array) explode: true",
- route: "/{.color*}",
- param: `@path color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: ".blue.black.brown",
- },
- {
- desc: "label (object) explode: false",
- route: "/{.color}",
- param: `@path color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: ".R,100,G,200,B,150",
- },
- {
- desc: "label (object) explode: true",
- route: "/{.color*}",
- param: `@path color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: ".R=100.G=200.B=150",
- },
- ])("$desc", async ({ param, route, paramExample, expectedExample }) => {
- const res: OpenAPI3Document = await openApiFor(
- `
- @opExample(#{
- parameters: #{
- color: ${paramExample},
- },
- })
- @route("${route}")
- op getColors(${param}): void;
- `,
- undefined,
- { "serialize-parameter-examples": true },
- );
- expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- });
- });
-
- describe("set example on the header parameter with serialization enabled", () => {
- it.each([
- {
- desc: "simple (undefined)",
- param: `@header color: string | null`,
- paramExample: `null`,
- expectedExample: "",
- },
- {
- desc: "simple (string)",
- param: `@header color: string`,
- paramExample: `"blue"`,
- expectedExample: "blue",
- },
- {
- desc: "simple (array) explode: false",
- param: `@header color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "blue,black,brown",
- },
- {
- desc: "simple (array) explode: true",
- param: `@header(#{ explode: true }) color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "blue,black,brown",
- },
- {
- desc: "simple (object) explode: false",
- param: `@header color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "R,100,G,200,B,150",
- },
- {
- desc: "simple (object) explode: true",
- param: `@header(#{ explode: true }) color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "R=100,G=200,B=150",
- },
- ])("$desc", async ({ param, paramExample, expectedExample }) => {
- const res: OpenAPI3Document = await openApiFor(
- `
- @opExample(#{
- parameters: #{
- color: ${paramExample},
- },
- })
- @route("/")
- op getColors(${param}): void;
- `,
- undefined,
- { "serialize-parameter-examples": true },
- );
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- });
- });
-
- describe("set example on the cookie parameter with serialization enabled", () => {
- it.each([
- {
- desc: "form (undefined)",
- param: `@cookie color: string | null`,
- paramExample: `null`,
- expectedExample: "color=",
- },
- {
- desc: "form (string)",
- param: `@cookie color: string`,
- paramExample: `"blue"`,
- expectedExample: "color=blue",
- },
- {
- desc: "form (array) explode: false",
- param: `@cookie color: string[]`,
- paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "color=blue,black,brown",
- },
- {
- desc: "form (object) explode: false",
- param: `@cookie color: Record`,
- paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "color=R,100,G,200,B,150",
- },
- ])("$desc", async ({ param, paramExample, expectedExample }) => {
- const res: OpenAPI3Document = await openApiFor(
- `
- @opExample(#{
- parameters: #{
- color: ${paramExample},
- },
- })
- @route("/")
- op getColors(${param}): void;
- `,
- undefined,
- { "serialize-parameter-examples": true },
- );
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
- expectedExample,
- );
- });
- });
-
- it("supports multiple examples on parameter with serialization enabled", async () => {
- const res = await openApiFor(
- `
- @opExample(#{
- parameters: #{
- color: "green",
- },
- }, #{ title: "MyExample" })
- @opExample(#{
- parameters: #{
- color: "red",
- },
- }, #{ title: "MyExample2" })
- @route("/")
- op getColors(@query color: string): void;
- `,
- undefined,
- { "serialize-parameter-examples": true },
- );
- expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
- MyExample: {
- summary: "MyExample",
- value: "?color=green",
- },
- MyExample2: {
- summary: "MyExample2",
- value: "?color=red",
- },
- });
- });
-
it("supports multiple examples on parameter without serialization enabled", async () => {
const res = await openApiFor(
`
From ddcc61d3c71aab86d13fb51965c201ae680acfa8 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Thu, 22 May 2025 13:27:11 -0700
Subject: [PATCH 07/14] add tests for encoded examples
---
packages/openapi3/test/examples.test.ts | 43 +++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 74a57f16b3e..abeca8807d7 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -307,4 +307,47 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
},
});
});
+
+ it("supports encoding", async () => {
+ const res = await openApiFor(`
+ @opExample(#{
+ parameters: #{
+ dob: plainDate.fromISO("2021-01-01"),
+ utc: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ utcAsUnix: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ dur: duration.fromISO("PT1H"),
+ }
+ }, #{ title: "Test Example"})
+ @route("/")
+ op getDates(...Test): void;
+
+ model Test {
+ @query
+ dob: plainDate;
+
+ @query
+ utc: utcDateTime;
+
+ @query
+ @encode(DateTimeKnownEncoding.unixTimestamp, int32)
+ utcAsUnix: utcDateTime;
+
+ @query
+ @encode(DurationKnownEncoding.seconds, int32)
+ dur: duration;
+ }
+ `);
+ expect((res.components.parameters["Test.dob"] as OpenAPI3Parameter).examples).toEqual({
+ "Test Example": { summary: "Test Example", value: "2021-01-01" },
+ });
+ expect((res.components.parameters["Test.utc"] as OpenAPI3Parameter).examples).toEqual({
+ "Test Example": { summary: "Test Example", value: "2021-01-01T00:00:00Z" },
+ });
+ expect((res.components.parameters["Test.utcAsUnix"] as OpenAPI3Parameter).examples).toEqual({
+ "Test Example": { summary: "Test Example", value: 1609459200 },
+ });
+ expect((res.components.parameters["Test.dur"] as OpenAPI3Parameter).examples).toEqual({
+ "Test Example": { summary: "Test Example", value: 3600 },
+ });
+ });
});
From 41dd053ae1c6a72471c2fa58652d3d5b930576bc Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Fri, 23 May 2025 09:57:24 -0700
Subject: [PATCH 08/14] revert docs
---
packages/openapi3/README.md | 8 --------
.../docs/docs/emitters/openapi3/reference/emitter.md | 8 --------
2 files changed, 16 deletions(-)
diff --git a/packages/openapi3/README.md b/packages/openapi3/README.md
index 779e23868ea..f650ab8a3f2 100644
--- a/packages/openapi3/README.md
+++ b/packages/openapi3/README.md
@@ -125,14 +125,6 @@ If true, then for models emitted as object schemas we default `additionalPropert
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
-### `serialize-parameter-examples`
-
-**Type:** `boolean`
-
-If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
-Default: `false`
-See https://spec.openapis.org/oas/v3.0.4.html#style-examples
-
## Decorators
### TypeSpec.OpenAPI
diff --git a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
index bc437b1743e..66b71c12c7d 100644
--- a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
+++ b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
@@ -118,11 +118,3 @@ Default: `int64`
If true, then for models emitted as object schemas we default `additionalProperties` to false for
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
-
-### `serialize-parameter-examples`
-
-**Type:** `boolean`
-
-If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
-Default: `false`
-See https://spec.openapis.org/oas/v3.0.4.html#style-examples
From 995e840ba5ce20f549a0a54b9b7f4111216c90ef Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Mon, 2 Jun 2025 13:41:36 -0700
Subject: [PATCH 09/14] Revert "remove parameter example serialization"
This reverts commit 30a082decf8d0664cf68fda64b6edc21276613f6.
---
packages/openapi3/src/examples.ts | 360 ++++++++++++++++++++-
packages/openapi3/src/lib.ts | 17 +
packages/openapi3/src/openapi.ts | 10 +-
packages/openapi3/test/examples.test.ts | 414 ++++++++++++++++++++++++
4 files changed, 798 insertions(+), 3 deletions(-)
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 86fe4510a44..14244765d13 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -1,13 +1,17 @@
import {
+ BooleanValue,
Example,
getOpExamples,
ignoreDiagnostics,
+ NumericValue,
OpExample,
Program,
serializeValueAsJson,
+ StringValue,
Type,
Value,
} from "@typespec/compiler";
+import { $ } from "@typespec/compiler/typekit";
import type {
HttpOperation,
HttpOperationResponse,
@@ -15,6 +19,7 @@ import type {
HttpProperty,
HttpStatusCodeRange,
} from "@typespec/http";
+import { getParameterStyle } from "./parameters.js";
import { getOpenAPI3StatusCodes } from "./status-codes.js";
import { OpenAPI3Example, OpenAPI3MediaType } from "./types.js";
import {
@@ -30,9 +35,14 @@ export interface OperationExamples {
responses: Record>;
}
+type ResolveOperationExamplesOptions = {
+ enableParameterSerialization: boolean;
+};
+
export function resolveOperationExamples(
program: Program,
operation: HttpOperation | SharedHttpOperation,
+ { enableParameterSerialization }: ResolveOperationExamplesOptions,
): OperationExamples {
const examples = findOperationExamples(program, operation);
const result: OperationExamples = { requestBody: {}, parameters: {}, responses: {} };
@@ -62,7 +72,12 @@ export function resolveOperationExamples(
for (const property of op.parameters.properties) {
if (!isHttpParameterProperty(property)) continue;
- const value = getParameterValue(program, example.parameters, property);
+ const value = getParameterValue(
+ program,
+ example.parameters,
+ property,
+ enableParameterSerialization,
+ );
if (value) {
const parameterName = property.options.name;
result.parameters[parameterName] ??= [];
@@ -263,11 +278,354 @@ function getParameterValue(
program: Program,
parameterExamples: Value,
property: HttpParameterProperties,
+ enableSerialization: boolean = false,
): Value | undefined {
const value = getValueByPath(parameterExamples, property.path);
+ if (!enableSerialization) {
+ return value;
+ }
+
+ if (!value) return value;
+
+ // Depending on the parameter type, we may need to serialize the value differently.
+ // https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ /*
+ Supported styles per location: https://spec.openapis.org/oas/v3.0.4.html#style-values
+ | Location | Default Style | Supported Styles |
+ | -------- | ------------- | ----------------------------------------------- |
+ | query | form | form, spaceDelimited, pipeDelimited, deepObject |
+ | header | simple | simple |
+ | path | simple | simple, label, matrix |
+ | cookie | form | form |
+ */
+ // explode is only relevant for array/object types
+
+ if (property.kind === "query") {
+ return getQueryParameterValue(program, value, property);
+ } else if (property.kind === "header") {
+ return getHeaderParameterValue(program, value, property);
+ } else if (property.kind === "path") {
+ return getPathParameterValue(program, value, property);
+ } else if (property.kind === "cookie") {
+ return getCookieParameterValue(program, value, property);
+ }
+
return value;
}
+function getQueryParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const style = getParameterStyle(program, property.property) ?? "form";
+
+ switch (style) {
+ case "form":
+ return getParameterFormValue(program, originalValue, property);
+ case "spaceDelimited":
+ return getParameterDelimitedValue(program, originalValue, property, " ");
+ case "pipeDelimited":
+ return getParameterDelimitedValue(program, originalValue, property, "|");
+ }
+}
+
+function getHeaderParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ return getParameterSimpleValue(program, originalValue, property);
+}
+
+function getPathParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { style } = property.options;
+
+ if (style === "label") {
+ return getParameterLabelValue(program, originalValue, property);
+ } else if (style === "matrix") {
+ return getParameterMatrixValue(program, originalValue, property);
+ } else if (style === "simple") {
+ return getParameterSimpleValue(program, originalValue, property);
+ }
+
+ return undefined;
+}
+
+function getCookieParameterValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ return getParameterFormValue(program, originalValue, property);
+}
+
+function getParameterLabelValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { explode } = property.options;
+ const tk = $(program);
+
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------- | ----------------- | ------------------ |
+ | false | .blue | .blue,black,brown | .R,100,G,200,B,150 |
+ | true | .blue | .blue.black.brown | .R=100.G=200.B=150 |
+ */
+
+ const joiner = explode ? "." : ",";
+ if (tk.value.isArray(originalValue)) {
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${value.value}`);
+ }
+ return tk.value.createString(`.${pairs.join(joiner)}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ const sep = explode ? "=" : ",";
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(`.${pairs.join(joiner)}`);
+ }
+
+ // null (undefined) is treated as a a dot
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString(".");
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return tk.value.createString(`.${originalValue.value}`);
+ }
+
+ return;
+}
+
+function getParameterMatrixValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { explode, name } = property.options;
+ const tk = $(program);
+
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------------- | ----------------------------------- | ------------------------ |
+ | false | ;color=blue | ;color=blue,black,brown | ;color=R,100,G,200,B,150 |
+ | true | ;color=blue | ;color=blue;color=black;color=brown | ;R=100;G=200;B=150 |
+ */
+
+ const joiner = explode ? ";" : ",";
+ const prefix = explode ? "" : `${name}=`;
+ if (tk.value.isArray(originalValue)) {
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
+ }
+ return tk.value.createString(`;${prefix}${pairs.join(joiner)}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const sep = explode ? "=" : ",";
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(`;${prefix}${pairs.join(joiner)}`);
+ }
+
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString(`;${name}`);
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return tk.value.createString(`;${name}=${originalValue.value}`);
+ }
+
+ return;
+}
+
+function getParameterDelimitedValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+ delimiter: " " | "|",
+): Value | undefined {
+ const { explode, name } = property.options;
+ // Serialization is undefined for explode=true
+ if (explode) return undefined;
+
+ const tk = $(program);
+ // cspell: ignore Cblack Cbrown
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | style | explode | string | array | object |
+ | ----- | ------- | ------ | --------------------------- | ---------------------------------- |
+ | pipe | false | n/a | ?color=blue%7Cblack%7Cbrown | ?color=R%7C100%7CG%7C200%7CB%7C150 |
+ | pipe | true | n/a | n/a | n/a |
+ | space | false | n/a | ?color=blue%20black%20brown | ?color=R%20100%20G%20200%20B%20150 |
+ | space | true | n/a | n/a | n/a |
+ */
+
+ if (tk.value.isArray(originalValue)) {
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${value.value}`);
+ }
+ return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${key}${delimiter}${value.value}`);
+ }
+ return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
+ }
+
+ return undefined;
+}
+
+function getParameterFormValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { name } = property.options;
+ const isCookie = property.kind === "cookie";
+ const explode = isCookie ? false : property.options.explode;
+ const tk = $(program);
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------------- | ----------------------------------- | ------------------------ |
+ | false | ?color=blue | ?color=blue,black,brown | ?color=R,100,G,200,B,150 |
+ | true | ?color=blue | ?color=blue&color=black&color=brown | ?R=100&G=200&B=150 |
+ */
+
+ const qPrefix = isCookie ? "" : "?";
+ const prefix = explode ? "" : `${name}=`;
+ if (tk.value.isArray(originalValue)) {
+ const sep = explode ? "&" : ",";
+ const pairs: string[] = [];
+ for (const value of originalValue.values) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
+ }
+ return tk.value.createString(`${qPrefix}${prefix}${pairs.join(sep)}`);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const sep = explode ? "=" : ",";
+ const joiner = explode ? "&" : ",";
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(`${qPrefix}${prefix}${pairs.join(joiner)}`);
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return tk.value.createString(`${qPrefix}${name}=${originalValue.value}`);
+ }
+
+ // null is treated as the 'undefined' value
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString(`${qPrefix}${name}=`);
+ }
+
+ return;
+}
+
+function getParameterSimpleValue(
+ program: Program,
+ originalValue: Value,
+ property: Extract,
+): Value | undefined {
+ const { explode } = property.options;
+ const tk = $(program);
+
+ /*
+ https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ string -> "blue"
+ array -> ["blue", "black", "brown"]
+ object -> { "R": 100, "G": 200, "B": 150 }
+
+ | explode | string | array | object |
+ | ------- | ------ | ---------------- | ----------------- |
+ | false | blue | blue,black,brown | R,100,G,200,B,150 |
+ | true | blue | blue,black,brown | R=100,G=200,B=150 |
+ */
+
+ if (tk.value.isArray(originalValue)) {
+ const serializedValue = originalValue.values
+ .filter(isSerializableScalarValue)
+ .map((v) => v.value)
+ .join(",");
+ return tk.value.createString(serializedValue);
+ }
+
+ if (tk.value.isObject(originalValue)) {
+ const pairs: string[] = [];
+ for (const [key, { value }] of originalValue.properties) {
+ if (!isSerializableScalarValue(value)) continue;
+ const sep = explode ? "=" : ",";
+ pairs.push(`${key}${sep}${value.value}`);
+ }
+ return tk.value.createString(pairs.join(","));
+ }
+
+ // null (undefined) is treated as an empty string - unrelated to allowEmptyValue
+ if (tk.value.isNull(originalValue)) {
+ return tk.value.createString("");
+ }
+
+ if (isSerializableScalarValue(originalValue)) {
+ return originalValue;
+ }
+
+ return;
+}
+
+function isSerializableScalarValue(
+ value: Value,
+): value is BooleanValue | NumericValue | StringValue {
+ return ["BooleanValue", "NumericValue", "StringValue"].includes(value.valueKind);
+}
+
function getValueByPath(value: Value, path: (string | number)[]): Value | undefined {
let current: Value | undefined = value;
for (const key of path) {
diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts
index cbf01f1ae29..ebedd31262b 100644
--- a/packages/openapi3/src/lib.ts
+++ b/packages/openapi3/src/lib.ts
@@ -81,6 +81,13 @@ export interface OpenAPI3EmitterOptions {
* @default false
*/
"seal-object-schemas"?: boolean;
+
+ /**
+ * If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
+ * @default false
+ * @see https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ */
+ "serialize-parameter-examples"?: boolean;
}
const EmitterOptionsSchema: JSONSchemaType = {
@@ -180,6 +187,16 @@ const EmitterOptionsSchema: JSONSchemaType = {
"Default: `false`",
].join("\n"),
},
+ "serialize-parameter-examples": {
+ type: "boolean",
+ nullable: true,
+ default: false,
+ description: [
+ "If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.",
+ "Default: `false`",
+ "See https://spec.openapis.org/oas/v3.0.4.html#style-examples",
+ ].join("\n"),
+ },
},
required: [],
};
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index 74d22b16a23..dfee294b504 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -211,6 +211,7 @@ export function resolveOptions(
outputFile: resolvePath(context.emitterOutputDir, specDir, outputFile),
openapiVersions,
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
+ serializeParameterExamples: resolvedOptions["serialize-parameter-examples"] ?? false,
};
}
@@ -223,6 +224,7 @@ export interface ResolvedOpenAPI3EmitterOptions {
includeXTypeSpecName: "inline-only" | "never";
safeintStrategy: "double-int" | "int64";
sealObjectSchemas: boolean;
+ serializeParameterExamples: boolean;
}
function createOAPIEmitter(
@@ -732,7 +734,9 @@ function createOAPIEmitter(
const operations = shared.operations;
const verb = operations[0].verb;
const path = operations[0].path;
- const examples = resolveOperationExamples(program, shared);
+ const examples = resolveOperationExamples(program, shared, {
+ enableParameterSerialization: options.serializeParameterExamples,
+ });
const oai3Operation: OpenAPI3Operation = {
operationId: computeSharedOperationId(shared),
parameters: [],
@@ -810,7 +814,9 @@ function createOAPIEmitter(
return undefined;
}
const visibility = resolveRequestVisibility(program, operation.operation, verb);
- const examples = resolveOperationExamples(program, operation);
+ const examples = resolveOperationExamples(program, operation, {
+ enableParameterSerialization: options.serializeParameterExamples,
+ });
const oai3Operation: OpenAPI3Operation = {
operationId: resolveOperationId(program, operation.operation),
summary: getSummary(program, operation.operation),
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index abeca8807d7..06683e153af 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -279,6 +279,420 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
},
);
+ describe("set example on the query parameter with serialization enabled", () => {
+ it.each([
+ {
+ desc: "form (undefined)",
+ param: `@query color: string | null`,
+ paramExample: `null`,
+ expectedExample: "?color=",
+ },
+ {
+ desc: "form (string)",
+ param: `@query color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "?color=blue",
+ },
+ {
+ desc: "form (array) explode: false",
+ param: `@query color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue,black,brown",
+ },
+ {
+ desc: "form (array) explode: true",
+ param: `@query(#{ explode: true }) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue&color=black&color=brown",
+ },
+ {
+ desc: "form (object) explode: false",
+ param: `@query color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?color=R,100,G,200,B,150",
+ },
+ {
+ desc: "form (object) explode: true",
+ param: `@query(#{ explode: true }) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?R=100&G=200&B=150",
+ },
+ {
+ desc: "spaceDelimited (undefined)",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: string | null`,
+ paramExample: `null`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "spaceDelimited (string)",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: string`,
+ paramExample: `"blue"`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "spaceDelimited (array) explode: false",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "?color=blue%20black%20brown",
+ },
+ {
+ desc: "spaceDelimited (array) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.spaceDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "spaceDelimited (object) explode: false",
+ param: `@query @encode(ArrayEncoding.spaceDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?color=R%20100%20G%20200%20B%20150",
+ },
+ {
+ desc: "spaceDelimited (object) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.spaceDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (undefined)",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: string | null`,
+ paramExample: `null`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (string)",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: string`,
+ paramExample: `"blue"`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (array) explode: false",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ // cspell:disable-next-line
+ expectedExample: "?color=blue%7Cblack%7Cbrown",
+ },
+ {
+ desc: "pipeDelimited (array) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.pipeDelimited) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: undefined,
+ },
+ {
+ desc: "pipeDelimited (object) explode: false",
+ param: `@query @encode(ArrayEncoding.pipeDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "?color=R%7C100%7CG%7C200%7CB%7C150",
+ },
+ {
+ desc: "pipeDelimited (object) explode: true",
+ param: `@query(#{ explode: true }) @encode(ArrayEncoding.pipeDelimited) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: undefined,
+ },
+ ])("$desc", async ({ param, paramExample, expectedExample }) => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+
+ describe("set example on the path parameter with serialization enabled", () => {
+ it.each([
+ {
+ desc: "simple (undefined)",
+ route: "/{color}",
+ param: `@path color: string | null`,
+ paramExample: `null`,
+ expectedExample: "",
+ },
+ {
+ desc: "simple (string)",
+ route: "/{color}",
+ param: `@path color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "blue",
+ },
+ {
+ desc: "simple (array) explode: false",
+ route: "/{color}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (array) explode: true",
+ route: "/{color*}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (object) explode: false",
+ route: "/{color}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R,100,G,200,B,150",
+ },
+ {
+ desc: "simple (object) explode: true",
+ route: "/{color*}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R=100,G=200,B=150",
+ },
+ {
+ desc: "matrix (undefined)",
+ route: "/{;color}",
+ param: `@path color: string | null`,
+ paramExample: `null`,
+ expectedExample: ";color",
+ },
+ {
+ desc: "matrix (string)",
+ route: "/{;color}",
+ param: `@path color: string`,
+ paramExample: `"blue"`,
+ expectedExample: ";color=blue",
+ },
+ {
+ desc: "matrix (array) explode: false",
+ route: "/{;color}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ";color=blue,black,brown",
+ },
+ {
+ desc: "matrix (array) explode: true",
+ route: "/{;color*}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ";color=blue;color=black;color=brown",
+ },
+ {
+ desc: "matrix (object) explode: false",
+ route: "/{;color}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ";color=R,100,G,200,B,150",
+ },
+ {
+ desc: "matrix (object) explode: true",
+ route: "/{;color*}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ";R=100;G=200;B=150",
+ },
+ {
+ desc: "label (undefined)",
+ route: "/{.color}",
+ param: `@path color: string | null`,
+ paramExample: `null`,
+ expectedExample: ".",
+ },
+ {
+ desc: "label (string)",
+ route: "/{.color}",
+ param: `@path color: string`,
+ paramExample: `"blue"`,
+ expectedExample: ".blue",
+ },
+ {
+ desc: "label (array) explode: false",
+ route: "/{.color}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ".blue,black,brown",
+ },
+ {
+ desc: "label (array) explode: true",
+ route: "/{.color*}",
+ param: `@path color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: ".blue.black.brown",
+ },
+ {
+ desc: "label (object) explode: false",
+ route: "/{.color}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ".R,100,G,200,B,150",
+ },
+ {
+ desc: "label (object) explode: true",
+ route: "/{.color*}",
+ param: `@path color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: ".R=100.G=200.B=150",
+ },
+ ])("$desc", async ({ param, route, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("${route}")
+ op getColors(${param}): void;
+ `,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+
+ describe("set example on the header parameter with serialization enabled", () => {
+ it.each([
+ {
+ desc: "simple (undefined)",
+ param: `@header color: string | null`,
+ paramExample: `null`,
+ expectedExample: "",
+ },
+ {
+ desc: "simple (string)",
+ param: `@header color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "blue",
+ },
+ {
+ desc: "simple (array) explode: false",
+ param: `@header color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (array) explode: true",
+ param: `@header(#{ explode: true }) color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "blue,black,brown",
+ },
+ {
+ desc: "simple (object) explode: false",
+ param: `@header color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R,100,G,200,B,150",
+ },
+ {
+ desc: "simple (object) explode: true",
+ param: `@header(#{ explode: true }) color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "R=100,G=200,B=150",
+ },
+ ])("$desc", async ({ param, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+
+ describe("set example on the cookie parameter with serialization enabled", () => {
+ it.each([
+ {
+ desc: "form (undefined)",
+ param: `@cookie color: string | null`,
+ paramExample: `null`,
+ expectedExample: "color=",
+ },
+ {
+ desc: "form (string)",
+ param: `@cookie color: string`,
+ paramExample: `"blue"`,
+ expectedExample: "color=blue",
+ },
+ {
+ desc: "form (array) explode: false",
+ param: `@cookie color: string[]`,
+ paramExample: `#["blue", "black", "brown"]`,
+ expectedExample: "color=blue,black,brown",
+ },
+ {
+ desc: "form (object) explode: false",
+ param: `@cookie color: Record`,
+ paramExample: `#{R: 100, G: 200, B: 150}`,
+ expectedExample: "color=R,100,G,200,B,150",
+ },
+ ])("$desc", async ({ param, paramExample, expectedExample }) => {
+ const res: OpenAPI3Document = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: ${paramExample},
+ },
+ })
+ @route("/")
+ op getColors(${param}): void;
+ `,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
+ expectedExample,
+ );
+ });
+ });
+
+ it("supports multiple examples on parameter with serialization enabled", async () => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: "green",
+ },
+ }, #{ title: "MyExample" })
+ @opExample(#{
+ parameters: #{
+ color: "red",
+ },
+ }, #{ title: "MyExample2" })
+ @route("/")
+ op getColors(@query color: string): void;
+ `,
+ undefined,
+ { "serialize-parameter-examples": true },
+ );
+ expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
+ MyExample: {
+ summary: "MyExample",
+ value: "?color=green",
+ },
+ MyExample2: {
+ summary: "MyExample2",
+ value: "?color=red",
+ },
+ });
+ });
+
it("supports multiple examples on parameter without serialization enabled", async () => {
const res = await openApiFor(
`
From ff7b31e68ccfff53ecb9fa63bace3ca3d9ece754 Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Mon, 2 Jun 2025 14:15:11 -0700
Subject: [PATCH 10/14] add back in parameter example serialization
---
packages/openapi3/src/examples.ts | 13 ++++++-------
packages/openapi3/test/examples.test.ts | 24 ++++++++++++------------
2 files changed, 18 insertions(+), 19 deletions(-)
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 14244765d13..74076cc47f4 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -499,7 +499,7 @@ function getParameterDelimitedValue(
if (!isSerializableScalarValue(value)) continue;
pairs.push(`${value.value}`);
}
- return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
+ return tk.value.createString(`${name}=${encodeURIComponent(pairs.join(delimiter))}`);
}
if (tk.value.isObject(originalValue)) {
@@ -508,7 +508,7 @@ function getParameterDelimitedValue(
if (!isSerializableScalarValue(value)) continue;
pairs.push(`${key}${delimiter}${value.value}`);
}
- return tk.value.createString(`?${name}=${encodeURIComponent(pairs.join(delimiter))}`);
+ return tk.value.createString(`${name}=${encodeURIComponent(pairs.join(delimiter))}`);
}
return undefined;
@@ -535,7 +535,6 @@ function getParameterFormValue(
| true | ?color=blue | ?color=blue&color=black&color=brown | ?R=100&G=200&B=150 |
*/
- const qPrefix = isCookie ? "" : "?";
const prefix = explode ? "" : `${name}=`;
if (tk.value.isArray(originalValue)) {
const sep = explode ? "&" : ",";
@@ -544,7 +543,7 @@ function getParameterFormValue(
if (!isSerializableScalarValue(value)) continue;
pairs.push(explode ? `${name}=${value.value}` : `${value.value}`);
}
- return tk.value.createString(`${qPrefix}${prefix}${pairs.join(sep)}`);
+ return tk.value.createString(`${prefix}${pairs.join(sep)}`);
}
if (tk.value.isObject(originalValue)) {
@@ -555,16 +554,16 @@ function getParameterFormValue(
if (!isSerializableScalarValue(value)) continue;
pairs.push(`${key}${sep}${value.value}`);
}
- return tk.value.createString(`${qPrefix}${prefix}${pairs.join(joiner)}`);
+ return tk.value.createString(`${prefix}${pairs.join(joiner)}`);
}
if (isSerializableScalarValue(originalValue)) {
- return tk.value.createString(`${qPrefix}${name}=${originalValue.value}`);
+ return tk.value.createString(`${name}=${originalValue.value}`);
}
// null is treated as the 'undefined' value
if (tk.value.isNull(originalValue)) {
- return tk.value.createString(`${qPrefix}${name}=`);
+ return tk.value.createString(`${name}=`);
}
return;
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 06683e153af..f1fbcb0ae8e 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -285,37 +285,37 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
desc: "form (undefined)",
param: `@query color: string | null`,
paramExample: `null`,
- expectedExample: "?color=",
+ expectedExample: "color=",
},
{
desc: "form (string)",
param: `@query color: string`,
paramExample: `"blue"`,
- expectedExample: "?color=blue",
+ expectedExample: "color=blue",
},
{
desc: "form (array) explode: false",
param: `@query color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "?color=blue,black,brown",
+ expectedExample: "color=blue,black,brown",
},
{
desc: "form (array) explode: true",
param: `@query(#{ explode: true }) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "?color=blue&color=black&color=brown",
+ expectedExample: "color=blue&color=black&color=brown",
},
{
desc: "form (object) explode: false",
param: `@query color: Record`,
paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?color=R,100,G,200,B,150",
+ expectedExample: "color=R,100,G,200,B,150",
},
{
desc: "form (object) explode: true",
param: `@query(#{ explode: true }) color: Record`,
paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?R=100&G=200&B=150",
+ expectedExample: "R=100&G=200&B=150",
},
{
desc: "spaceDelimited (undefined)",
@@ -333,7 +333,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
desc: "spaceDelimited (array) explode: false",
param: `@query @encode(ArrayEncoding.spaceDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
- expectedExample: "?color=blue%20black%20brown",
+ expectedExample: "color=blue%20black%20brown",
},
{
desc: "spaceDelimited (array) explode: true",
@@ -345,7 +345,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
desc: "spaceDelimited (object) explode: false",
param: `@query @encode(ArrayEncoding.spaceDelimited) color: Record`,
paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?color=R%20100%20G%20200%20B%20150",
+ expectedExample: "color=R%20100%20G%20200%20B%20150",
},
{
desc: "spaceDelimited (object) explode: true",
@@ -370,7 +370,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
param: `@query @encode(ArrayEncoding.pipeDelimited) color: string[]`,
paramExample: `#["blue", "black", "brown"]`,
// cspell:disable-next-line
- expectedExample: "?color=blue%7Cblack%7Cbrown",
+ expectedExample: "color=blue%7Cblack%7Cbrown",
},
{
desc: "pipeDelimited (array) explode: true",
@@ -382,7 +382,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
desc: "pipeDelimited (object) explode: false",
param: `@query @encode(ArrayEncoding.pipeDelimited) color: Record`,
paramExample: `#{R: 100, G: 200, B: 150}`,
- expectedExample: "?color=R%7C100%7CG%7C200%7CB%7C150",
+ expectedExample: "color=R%7C100%7CG%7C200%7CB%7C150",
},
{
desc: "pipeDelimited (object) explode: true",
@@ -684,11 +684,11 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
MyExample: {
summary: "MyExample",
- value: "?color=green",
+ value: "color=green",
},
MyExample2: {
summary: "MyExample2",
- value: "?color=red",
+ value: "color=red",
},
});
});
From 61d52d798ce6289a382ee6d90f0a6f34d7523abd Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Mon, 2 Jun 2025 14:18:23 -0700
Subject: [PATCH 11/14] add experimental parameter examples option to opt into
example emission strategy
---
packages/openapi3/src/examples.ts | 39 ++++++++++++++-----------
packages/openapi3/src/lib.ts | 18 +++++++-----
packages/openapi3/src/openapi.ts | 8 ++---
packages/openapi3/test/examples.test.ts | 38 +++++++++++++++++++-----
4 files changed, 67 insertions(+), 36 deletions(-)
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 74076cc47f4..9cf15d2be23 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -19,6 +19,7 @@ import type {
HttpProperty,
HttpStatusCodeRange,
} from "@typespec/http";
+import { ExperimentalParameterExamplesStrategy } from "./lib.js";
import { getParameterStyle } from "./parameters.js";
import { getOpenAPI3StatusCodes } from "./status-codes.js";
import { OpenAPI3Example, OpenAPI3MediaType } from "./types.js";
@@ -36,13 +37,13 @@ export interface OperationExamples {
}
type ResolveOperationExamplesOptions = {
- enableParameterSerialization: boolean;
+ parameterExamplesStrategy?: ExperimentalParameterExamplesStrategy;
};
export function resolveOperationExamples(
program: Program,
operation: HttpOperation | SharedHttpOperation,
- { enableParameterSerialization }: ResolveOperationExamplesOptions,
+ { parameterExamplesStrategy }: ResolveOperationExamplesOptions,
): OperationExamples {
const examples = findOperationExamples(program, operation);
const result: OperationExamples = { requestBody: {}, parameters: {}, responses: {} };
@@ -76,7 +77,7 @@ export function resolveOperationExamples(
program,
example.parameters,
property,
- enableParameterSerialization,
+ parameterExamplesStrategy,
);
if (value) {
const parameterName = property.options.name;
@@ -278,15 +279,19 @@ function getParameterValue(
program: Program,
parameterExamples: Value,
property: HttpParameterProperties,
- enableSerialization: boolean = false,
+ parameterExamplesStrategy?: ExperimentalParameterExamplesStrategy,
): Value | undefined {
+ if (!parameterExamplesStrategy) {
+ return;
+ }
+
const value = getValueByPath(parameterExamples, property.path);
- if (!enableSerialization) {
+ if (!value) return;
+
+ if (parameterExamplesStrategy === "data") {
return value;
}
- if (!value) return value;
-
// Depending on the parameter type, we may need to serialize the value differently.
// https://spec.openapis.org/oas/v3.0.4.html#style-examples
/*
@@ -485,12 +490,12 @@ function getParameterDelimitedValue(
array -> ["blue", "black", "brown"]
object -> { "R": 100, "G": 200, "B": 150 }
- | style | explode | string | array | object |
- | ----- | ------- | ------ | --------------------------- | ---------------------------------- |
- | pipe | false | n/a | ?color=blue%7Cblack%7Cbrown | ?color=R%7C100%7CG%7C200%7CB%7C150 |
- | pipe | true | n/a | n/a | n/a |
- | space | false | n/a | ?color=blue%20black%20brown | ?color=R%20100%20G%20200%20B%20150 |
- | space | true | n/a | n/a | n/a |
+ | style | explode | string | array | object |
+ | ----- | ------- | ------ | -------------------------- | ----------------------------------|
+ | pipe | false | n/a | color=blue%7Cblack%7Cbrown | color=R%7C100%7CG%7C200%7CB%7C150 |
+ | pipe | true | n/a | n/a | n/a |
+ | space | false | n/a | color=blue%20black%20brown | color=R%20100%20G%20200%20B%20150 |
+ | space | true | n/a | n/a | n/a |
*/
if (tk.value.isArray(originalValue)) {
@@ -529,10 +534,10 @@ function getParameterFormValue(
array -> ["blue", "black", "brown"]
object -> { "R": 100, "G": 200, "B": 150 }
- | explode | string | array | object |
- | ------- | ------------- | ----------------------------------- | ------------------------ |
- | false | ?color=blue | ?color=blue,black,brown | ?color=R,100,G,200,B,150 |
- | true | ?color=blue | ?color=blue&color=black&color=brown | ?R=100&G=200&B=150 |
+ | explode | string | array | object |
+ | ------- | ------------ | ---------------------------------- | ----------------------- |
+ | false | color=blue | color=blue,black,brown | color=R,100,G,200,B,150 |
+ | true | color=blue | color=blue&color=black&color=brown | R=100&G=200&B=150 |
*/
const prefix = explode ? "" : `${name}=`;
diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts
index ebedd31262b..b6a69afc78f 100644
--- a/packages/openapi3/src/lib.ts
+++ b/packages/openapi3/src/lib.ts
@@ -2,6 +2,7 @@ import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/c
export type FileType = "yaml" | "json";
export type OpenAPIVersion = "3.0.0" | "3.1.0";
+export type ExperimentalParameterExamplesStrategy = "data" | "serialized";
export interface OpenAPI3EmitterOptions {
/**
* If the content should be serialized as YAML or JSON.
@@ -83,11 +84,12 @@ export interface OpenAPI3EmitterOptions {
"seal-object-schemas"?: boolean;
/**
- * If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.
- * @default false
+ * Determines how to emit examples on parameters.
+ *
+ * Note: This is an experimental feature and may change in future versions.
* @see https://spec.openapis.org/oas/v3.0.4.html#style-examples
*/
- "serialize-parameter-examples"?: boolean;
+ "experimental-parameter-examples"?: ExperimentalParameterExamplesStrategy;
}
const EmitterOptionsSchema: JSONSchemaType = {
@@ -187,13 +189,13 @@ const EmitterOptionsSchema: JSONSchemaType = {
"Default: `false`",
].join("\n"),
},
- "serialize-parameter-examples": {
- type: "boolean",
+ "experimental-parameter-examples": {
+ type: "string",
+ enum: ["data", "serialized"],
nullable: true,
- default: false,
description: [
- "If true, then examples on parameters will be serialized based on the parameter's prescribed serialization strategy.",
- "Default: `false`",
+ "Determines how to emit examples on parameters.",
+ "Note: This is an experimental feature and may change in future versions.",
"See https://spec.openapis.org/oas/v3.0.4.html#style-examples",
].join("\n"),
},
diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts
index dfee294b504..8ddb0305388 100644
--- a/packages/openapi3/src/openapi.ts
+++ b/packages/openapi3/src/openapi.ts
@@ -211,7 +211,7 @@ export function resolveOptions(
outputFile: resolvePath(context.emitterOutputDir, specDir, outputFile),
openapiVersions,
sealObjectSchemas: resolvedOptions["seal-object-schemas"],
- serializeParameterExamples: resolvedOptions["serialize-parameter-examples"] ?? false,
+ parameterExamplesStrategy: resolvedOptions["experimental-parameter-examples"],
};
}
@@ -224,7 +224,7 @@ export interface ResolvedOpenAPI3EmitterOptions {
includeXTypeSpecName: "inline-only" | "never";
safeintStrategy: "double-int" | "int64";
sealObjectSchemas: boolean;
- serializeParameterExamples: boolean;
+ parameterExamplesStrategy?: "data" | "serialized";
}
function createOAPIEmitter(
@@ -735,7 +735,7 @@ function createOAPIEmitter(
const verb = operations[0].verb;
const path = operations[0].path;
const examples = resolveOperationExamples(program, shared, {
- enableParameterSerialization: options.serializeParameterExamples,
+ parameterExamplesStrategy: options.parameterExamplesStrategy,
});
const oai3Operation: OpenAPI3Operation = {
operationId: computeSharedOperationId(shared),
@@ -815,7 +815,7 @@ function createOAPIEmitter(
}
const visibility = resolveRequestVisibility(program, operation.operation, verb);
const examples = resolveOperationExamples(program, operation, {
- enableParameterSerialization: options.serializeParameterExamples,
+ parameterExamplesStrategy: options.parameterExamplesStrategy,
});
const oai3Operation: OpenAPI3Operation = {
operationId: resolveOperationId(program, operation.operation),
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index f1fbcb0ae8e..8c8a304b0de 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -235,6 +235,22 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
+ it("does not set examples on parameters by default", async () => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ color: "blue",
+ },
+ })
+ @route("/")
+ op getColors(@query color: string): void;
+ `,
+ );
+ expect((res.paths["/"].get?.parameters[0] as OpenAPI3Parameter).example).toBeUndefined();
+ expect((res.paths["/"].get?.parameters[0] as OpenAPI3Parameter).examples).toBeUndefined();
+ });
+
describe.each(["path", "query", "header", "cookie"])(
"set example on the %s parameter without serialization",
(paramType) => {
@@ -271,6 +287,8 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@route("/")
op getColors(${param}): void;
`,
+ undefined,
+ { "experimental-parameter-examples": "data" },
);
expect((res.paths[path].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
expectedExample,
@@ -402,7 +420,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
op getColors(${param}): void;
`,
undefined,
- { "serialize-parameter-examples": true },
+ { "experimental-parameter-examples": "serialized" },
);
expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
expectedExample,
@@ -550,7 +568,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
op getColors(${param}): void;
`,
undefined,
- { "serialize-parameter-examples": true },
+ { "experimental-parameter-examples": "serialized" },
);
expect((res.paths[`/{color}`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
expectedExample,
@@ -608,7 +626,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
op getColors(${param}): void;
`,
undefined,
- { "serialize-parameter-examples": true },
+ { "experimental-parameter-examples": "serialized" },
);
expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
expectedExample,
@@ -654,7 +672,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
op getColors(${param}): void;
`,
undefined,
- { "serialize-parameter-examples": true },
+ { "experimental-parameter-examples": "serialized" },
);
expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).example).toEqual(
expectedExample,
@@ -679,7 +697,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
op getColors(@query color: string): void;
`,
undefined,
- { "serialize-parameter-examples": true },
+ { "experimental-parameter-examples": "serialized" },
);
expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
MyExample: {
@@ -709,6 +727,8 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@route("/")
op getColors(@query color: string): void;
`,
+ undefined,
+ { "experimental-parameter-examples": "data" },
);
expect((res.paths[`/`].get?.parameters[0] as OpenAPI3Parameter).examples).toEqual({
MyExample: {
@@ -723,7 +743,8 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
it("supports encoding", async () => {
- const res = await openApiFor(`
+ const res = await openApiFor(
+ `
@opExample(#{
parameters: #{
dob: plainDate.fromISO("2021-01-01"),
@@ -750,7 +771,10 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@encode(DurationKnownEncoding.seconds, int32)
dur: duration;
}
- `);
+ `,
+ undefined,
+ { "experimental-parameter-examples": "data" },
+ );
expect((res.components.parameters["Test.dob"] as OpenAPI3Parameter).examples).toEqual({
"Test Example": { summary: "Test Example", value: "2021-01-01" },
});
From 88cc8ac6290be2089448c2c2e731f63a01547aad Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Mon, 2 Jun 2025 15:51:26 -0700
Subject: [PATCH 12/14] correct encoding of headers in parameter examples
---
packages/openapi3/src/examples.ts | 57 ++++++++++++++---
packages/openapi3/test/examples.test.ts | 81 ++++++++++++++++++++++++-
2 files changed, 129 insertions(+), 9 deletions(-)
diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts
index 9cf15d2be23..d39223e2722 100644
--- a/packages/openapi3/src/examples.ts
+++ b/packages/openapi3/src/examples.ts
@@ -1,8 +1,11 @@
import {
BooleanValue,
+ EncodeData,
Example,
+ getEncode,
getOpExamples,
ignoreDiagnostics,
+ ModelProperty,
NumericValue,
OpExample,
Program,
@@ -12,12 +15,13 @@ import {
Value,
} from "@typespec/compiler";
import { $ } from "@typespec/compiler/typekit";
-import type {
- HttpOperation,
- HttpOperationResponse,
- HttpOperationResponseContent,
- HttpProperty,
- HttpStatusCodeRange,
+import {
+ isHeader,
+ type HttpOperation,
+ type HttpOperationResponse,
+ type HttpOperationResponseContent,
+ type HttpProperty,
+ type HttpStatusCodeRange,
} from "@typespec/http";
import { ExperimentalParameterExamplesStrategy } from "./lib.js";
import { getParameterStyle } from "./parameters.js";
@@ -212,6 +216,41 @@ function findResponseForExample(
return undefined;
}
+/**
+ * Only returns an encoding if one is not explicitly defined
+ */
+function getDefaultHeaderEncodeAs(program: Program, header: ModelProperty): EncodeData | undefined {
+ // Get existing encoded data if it has been explicitly defined
+ const encodeData = getEncode(program, header);
+ // If there's an explicit encoding, return undefined
+ if (encodeData) return;
+
+ const tk = $(program);
+
+ if (!tk.scalar.isUtcDateTime(header.type) && tk.scalar.isOffsetDateTime(header.type)) {
+ return;
+ }
+
+ if (!tk.scalar.is(header.type)) return;
+
+ // Use the default encoding for date-time headers
+ return {
+ encoding: "rfc7231",
+ type: header.type,
+ };
+}
+
+/**
+ * This function should only be used for special default encodings.
+ */
+function getEncodeAs(program: Program, type: Type): EncodeData | undefined {
+ if (isHeader(program, type)) {
+ return getDefaultHeaderEncodeAs(program, type as ModelProperty);
+ }
+
+ return undefined;
+}
+
export function getExampleOrExamples(
program: Program,
examples: [Example, Type][],
@@ -226,14 +265,16 @@ export function getExampleOrExamples(
examples[0][0].description === undefined
) {
const [example, type] = examples[0];
- return { example: serializeValueAsJson(program, example.value, type) };
+ const encodeAs = getEncodeAs(program, type);
+ return { example: serializeValueAsJson(program, example.value, type, encodeAs) };
} else {
const exampleObj: Record = {};
for (const [index, [example, type]] of examples.entries()) {
+ const encodeAs = getEncodeAs(program, type);
exampleObj[example.title ?? `example${index}`] = {
summary: example.title,
description: example.description,
- value: serializeValueAsJson(program, example.value, type),
+ value: serializeValueAsJson(program, example.value, type, encodeAs),
};
}
return { examples: exampleObj };
diff --git a/packages/openapi3/test/examples.test.ts b/packages/openapi3/test/examples.test.ts
index 8c8a304b0de..b9c01ed79d2 100644
--- a/packages/openapi3/test/examples.test.ts
+++ b/packages/openapi3/test/examples.test.ts
@@ -742,7 +742,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
});
});
- it("supports encoding", async () => {
+ it("supports encoding in examples", async () => {
const res = await openApiFor(
`
@opExample(#{
@@ -751,6 +751,8 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
utc: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
utcAsUnix: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
dur: duration.fromISO("PT1H"),
+ defaultHeader: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ encodedHeader: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
}
}, #{ title: "Test Example"})
@route("/")
@@ -770,6 +772,13 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
@query
@encode(DurationKnownEncoding.seconds, int32)
dur: duration;
+
+ @header
+ defaultHeader: utcDateTime;
+
+ @header
+ @encode(DateTimeKnownEncoding.rfc3339)
+ encodedHeader: utcDateTime;
}
`,
undefined,
@@ -787,5 +796,75 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
expect((res.components.parameters["Test.dur"] as OpenAPI3Parameter).examples).toEqual({
"Test Example": { summary: "Test Example", value: 3600 },
});
+ expect((res.components.parameters["Test.defaultHeader"] as OpenAPI3Parameter).examples).toEqual(
+ {
+ "Test Example": { summary: "Test Example", value: "Fri, 01 Jan 2021 00:00:00 GMT" },
+ },
+ );
+ expect((res.components.parameters["Test.encodedHeader"] as OpenAPI3Parameter).examples).toEqual(
+ {
+ "Test Example": { summary: "Test Example", value: "2021-01-01T00:00:00Z" },
+ },
+ );
+ });
+
+ it("supports encoding in example", async () => {
+ const res = await openApiFor(
+ `
+ @opExample(#{
+ parameters: #{
+ dob: plainDate.fromISO("2021-01-01"),
+ utc: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ utcAsUnix: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ dur: duration.fromISO("PT1H"),
+ defaultHeader: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ encodedHeader: utcDateTime.fromISO("2021-01-01T00:00:00Z"),
+ }
+ })
+ @route("/")
+ op getDates(...Test): void;
+
+ model Test {
+ @query
+ dob: plainDate;
+
+ @query
+ utc: utcDateTime;
+
+ @query
+ @encode(DateTimeKnownEncoding.unixTimestamp, int32)
+ utcAsUnix: utcDateTime;
+
+ @query
+ @encode(DurationKnownEncoding.seconds, int32)
+ dur: duration;
+
+ @header
+ defaultHeader: utcDateTime;
+
+ @header
+ @encode(DateTimeKnownEncoding.rfc3339)
+ encodedHeader: utcDateTime;
+ }
+ `,
+ undefined,
+ { "experimental-parameter-examples": "data" },
+ );
+ expect((res.components.parameters["Test.dob"] as OpenAPI3Parameter).example).toEqual(
+ "2021-01-01",
+ );
+ expect((res.components.parameters["Test.utc"] as OpenAPI3Parameter).example).toEqual(
+ "2021-01-01T00:00:00Z",
+ );
+ expect((res.components.parameters["Test.utcAsUnix"] as OpenAPI3Parameter).example).toEqual(
+ 1609459200,
+ );
+ expect((res.components.parameters["Test.dur"] as OpenAPI3Parameter).example).toEqual(3600);
+ expect((res.components.parameters["Test.defaultHeader"] as OpenAPI3Parameter).example).toEqual(
+ "Fri, 01 Jan 2021 00:00:00 GMT",
+ );
+ expect((res.components.parameters["Test.encodedHeader"] as OpenAPI3Parameter).example).toEqual(
+ "2021-01-01T00:00:00Z",
+ );
});
});
From e51e4cb9c45cdd9213df6caef38eb3abd124b01a Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 3 Jun 2025 08:55:23 -0700
Subject: [PATCH 13/14] update docs
---
.../changes/oa3-examples-on-params-2025-4-20-12-45-54.md | 2 +-
packages/openapi3/README.md | 8 ++++++++
.../docs/docs/emitters/openapi3/reference/emitter.md | 8 ++++++++
3 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md b/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md
index 7a9ed849dfd..526458f9f53 100644
--- a/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md
+++ b/.chronus/changes/oa3-examples-on-params-2025-4-20-12-45-54.md
@@ -4,4 +4,4 @@ packages:
- "@typespec/openapi3"
---
-Adds support for parameter examples via `@opExample`
\ No newline at end of file
+Adds support for parameter examples via `@opExample` via the `experimental-parameter-examples` option.
diff --git a/packages/openapi3/README.md b/packages/openapi3/README.md
index 3b34592204b..d7506d92f72 100644
--- a/packages/openapi3/README.md
+++ b/packages/openapi3/README.md
@@ -125,6 +125,14 @@ If true, then for models emitted as object schemas we default `additionalPropert
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
+### `experimental-parameter-examples`
+
+**Type:** `"data" | "serialized"`
+
+Determines how to emit examples on parameters.
+Note: This is an experimental feature and may change in future versions.
+See https://spec.openapis.org/oas/v3.0.4.html#style-examples
+
## Decorators
### TypeSpec.OpenAPI
diff --git a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
index e7246583da0..bf32451df62 100644
--- a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
+++ b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
@@ -118,3 +118,11 @@ Default: `int64`
If true, then for models emitted as object schemas we default `additionalProperties` to false for
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
+
+### `experimental-parameter-examples`
+
+**Type:** `"data" | "serialized"`
+
+Determines how to emit examples on parameters.
+Note: This is an experimental feature and may change in future versions.
+See https://spec.openapis.org/oas/v3.0.4.html#style-examples
From 2c3a41431c237b9dc9b29a36a25a8b800311229f Mon Sep 17 00:00:00 2001
From: Christopher Radek
Date: Tue, 3 Jun 2025 09:22:25 -0700
Subject: [PATCH 14/14] add link to discussion
---
packages/openapi3/README.md | 3 ++-
packages/openapi3/src/lib.ts | 6 ++++--
.../docs/docs/emitters/openapi3/reference/emitter.md | 3 ++-
3 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/packages/openapi3/README.md b/packages/openapi3/README.md
index d7506d92f72..792084912ad 100644
--- a/packages/openapi3/README.md
+++ b/packages/openapi3/README.md
@@ -131,7 +131,8 @@ Default: `false`
Determines how to emit examples on parameters.
Note: This is an experimental feature and may change in future versions.
-See https://spec.openapis.org/oas/v3.0.4.html#style-examples
+See https://spec.openapis.org/oas/v3.0.4.html#style-examples for parameter example serialization rules
+See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion on handling parameter examples.
## Decorators
diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts
index b6a69afc78f..ae5cee57c8b 100644
--- a/packages/openapi3/src/lib.ts
+++ b/packages/openapi3/src/lib.ts
@@ -87,7 +87,8 @@ export interface OpenAPI3EmitterOptions {
* Determines how to emit examples on parameters.
*
* Note: This is an experimental feature and may change in future versions.
- * @see https://spec.openapis.org/oas/v3.0.4.html#style-examples
+ * @see https://spec.openapis.org/oas/v3.0.4.html#style-examples for parameter example serialization rules.
+ * @see https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion on handling parameter examples.
*/
"experimental-parameter-examples"?: ExperimentalParameterExamplesStrategy;
}
@@ -196,7 +197,8 @@ const EmitterOptionsSchema: JSONSchemaType = {
description: [
"Determines how to emit examples on parameters.",
"Note: This is an experimental feature and may change in future versions.",
- "See https://spec.openapis.org/oas/v3.0.4.html#style-examples",
+ "See https://spec.openapis.org/oas/v3.0.4.html#style-examples for parameter example serialization rules",
+ "See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion on handling parameter examples.",
].join("\n"),
},
},
diff --git a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
index bf32451df62..726054886b8 100644
--- a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
+++ b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md
@@ -125,4 +125,5 @@ Default: `false`
Determines how to emit examples on parameters.
Note: This is an experimental feature and may change in future versions.
-See https://spec.openapis.org/oas/v3.0.4.html#style-examples
+See https://spec.openapis.org/oas/v3.0.4.html#style-examples for parameter example serialization rules
+See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion on handling parameter examples.