diff --git a/.chronus/changes/tuple-openapi3-fixed-length-2026-8-9-13-1-9.md b/.chronus/changes/tuple-openapi3-fixed-length-2026-8-9-13-1-9.md new file mode 100644 index 00000000000..994b15a3e88 --- /dev/null +++ b/.chronus/changes/tuple-openapi3-fixed-length-2026-8-9-13-1-9.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Emit `minItems` and `maxItems` for tuple types so generated schemas enforce the tuple's exact length. diff --git a/packages/openapi3/src/schema-emitter-3-1.ts b/packages/openapi3/src/schema-emitter-3-1.ts index 39c6d512a35..e58c7672197 100644 --- a/packages/openapi3/src/schema-emitter-3-1.ts +++ b/packages/openapi3/src/schema-emitter-3-1.ts @@ -389,9 +389,12 @@ export class OpenAPI31SchemaEmitter extends OpenAPI3SchemaEmitterBase> { + const size = tuple.values.length; return new ObjectBuilder({ type: "array", prefixItems: this.emitter.emitTupleLiteralValues(tuple), + minItems: size, + maxItems: size, }); } diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 2bb33ae4a31..ef9a4af8c8e 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -845,7 +845,8 @@ export class OpenAPI3SchemaEmitterBase< } tupleLiteral(tuple: Tuple): EmitterOutput> { - return { type: "array", items: {} }; + const size = tuple.values.length; + return { type: "array", items: {}, minItems: size, maxItems: size }; } #getSchemaForScalar(scalar: Scalar): Schema { let result: Schema = {} as Schema; diff --git a/packages/openapi3/test/array.test.ts b/packages/openapi3/test/array.test.ts index cc49faebbee..842f89934ea 100644 --- a/packages/openapi3/test/array.test.ts +++ b/packages/openapi3/test/array.test.ts @@ -268,21 +268,43 @@ worksFor(["3.0.0"], ({ oapiForModel }) => { deepStrictEqual(res.schemas.Pet.properties.names, { type: "array", items: {}, + minItems: 2, + maxItems: 2, default: ["bismarck", 12], }); deepStrictEqual(res.schemas.Pet.properties.decimals, { type: "array", items: {}, + minItems: 2, + maxItems: 2, default: ["hi", 456.7], }); deepStrictEqual(res.schemas.Pet.properties.decimal128s, { type: "array", items: {}, + minItems: 2, + maxItems: 2, default: ["hi", 456.7], }); }); + + it("emits minItems and maxItems of 0 for empty tuple", async () => { + const res = await oapiForModel( + "Pet", + ` + model Pet { names: [] }; + `, + ); + + deepStrictEqual(res.schemas.Pet.properties.names, { + type: "array", + items: {}, + minItems: 0, + maxItems: 0, + }); + }); }); worksFor(["3.1.0"], ({ oapiForModel }) => { @@ -369,18 +391,24 @@ worksFor(["3.1.0"], ({ oapiForModel }) => { deepStrictEqual(res.schemas.Pet.properties.names, { type: "array", prefixItems: [{ type: "string" }, { type: "integer", format: "int32" }], + minItems: 2, + maxItems: 2, default: ["bismarck", 12], }); deepStrictEqual(res.schemas.Pet.properties.decimals, { type: "array", prefixItems: [{ type: "string" }, { type: "number", format: "decimal" }], + minItems: 2, + maxItems: 2, default: ["hi", 456.7], }); deepStrictEqual(res.schemas.Pet.properties.decimal128s, { type: "array", prefixItems: [{ type: "string" }, { type: "number", format: "decimal128" }], + minItems: 2, + maxItems: 2, default: ["hi", 456.7], }); }); @@ -401,6 +429,8 @@ worksFor(["3.1.0"], ({ oapiForModel }) => { { type: "string", enum: ["bismark"] }, { type: "number", enum: [32] }, ], + minItems: 2, + maxItems: 2, }); }); }); @@ -418,6 +448,8 @@ worksFor(["3.1.0", "3.2.0"], ({ oapiForModel }) => { deepStrictEqual(res.schemas.Pet.properties.names, { type: "array", prefixItems: [{ $ref: "#/components/schemas/Bar" }], + minItems: 1, + maxItems: 1, }); }); @@ -432,6 +464,24 @@ worksFor(["3.1.0", "3.2.0"], ({ oapiForModel }) => { deepStrictEqual(res.schemas.Pet.properties.names, { type: "array", prefixItems: [{ $ref: "#/components/schemas/Pet" }], + minItems: 1, + maxItems: 1, + }); + }); + + it("emits minItems and maxItems of 0 for empty tuple", async () => { + const res = await oapiForModel( + "Pet", + ` + model Pet { names: [] }; + `, + ); + + deepStrictEqual(res.schemas.Pet.properties.names, { + type: "array", + prefixItems: [], + minItems: 0, + maxItems: 0, }); }); }); diff --git a/packages/samples/test/output/authentication/@typespec/openapi3/openapi.TypeSpec.ServiceAuth.yaml b/packages/samples/test/output/authentication/@typespec/openapi3/openapi.TypeSpec.ServiceAuth.yaml index a23f54f837b..76d8de1e8b2 100644 --- a/packages/samples/test/output/authentication/@typespec/openapi3/openapi.TypeSpec.ServiceAuth.yaml +++ b/packages/samples/test/output/authentication/@typespec/openapi3/openapi.TypeSpec.ServiceAuth.yaml @@ -47,6 +47,8 @@ components: scopes: type: array items: {} + minItems: 2 + maxItems: 2 securitySchemes: BearerAuth: type: http