From 2b472295b6403d38bf275334db7b9ac43f34e69d Mon Sep 17 00:00:00 2001 From: Pontus Lundin Date: Mon, 5 May 2025 09:48:26 +0200 Subject: [PATCH 1/6] add support for pattern properties --- .../src/transform/schema-object.ts | 20 ++++++++++++++----- packages/openapi-typescript/src/types.ts | 1 + 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 9c5725126..b52cd8c90 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -443,6 +443,7 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor if ( ("properties" in schemaObject && schemaObject.properties && Object.keys(schemaObject.properties).length) || ("additionalProperties" in schemaObject && schemaObject.additionalProperties) || + ("patternProperties" in schemaObject && schemaObject.patternProperties) || ("$defs" in schemaObject && schemaObject.$defs) ) { // properties @@ -542,13 +543,22 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor ); } - // additionalProperties - if (schemaObject.additionalProperties || options.ctx.additionalProperties) { + // additionalProperties / patternProperties + if (schemaObject.additionalProperties || options.ctx.additionalProperties || schemaObject.patternProperties) { const hasExplicitAdditionalProperties = typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length; - const addlType = hasExplicitAdditionalProperties - ? transformSchemaObject(schemaObject.additionalProperties as SchemaObject, options) - : UNKNOWN; + const hasExplicitPatternProperties = + typeof schemaObject.patternProperties === "object" && Object.keys(schemaObject.patternProperties).length; + const addlTypes = []; + if (hasExplicitAdditionalProperties) { + addlTypes.push(transformSchemaObject(schemaObject.additionalProperties as SchemaObject, options)); + } + if (hasExplicitPatternProperties) { + for (const [_, v] of getEntries(schemaObject.patternProperties ?? {}, options.ctx)) { + addlTypes.push(transformSchemaObject(v, options)); + } + } + const addlType = addlTypes.length === 0 ? UNKNOWN : tsUnion(addlTypes); return tsIntersection([ ...(coreObjectType.length ? [ts.factory.createTypeLiteralNode(coreObjectType)] : []), ts.factory.createTypeLiteralNode([ diff --git a/packages/openapi-typescript/src/types.ts b/packages/openapi-typescript/src/types.ts index 75d8f8c07..6cb985e71 100644 --- a/packages/openapi-typescript/src/types.ts +++ b/packages/openapi-typescript/src/types.ts @@ -502,6 +502,7 @@ export interface ObjectSubtype { type: "object" | ["object", "null"]; properties?: { [name: string]: SchemaObject | ReferenceObject }; additionalProperties?: boolean | Record | SchemaObject | ReferenceObject; + patternProperties?: Record; required?: string[]; allOf?: (SchemaObject | ReferenceObject)[]; anyOf?: (SchemaObject | ReferenceObject)[]; From 0349db8fe263721248d626514470fc7177201b6e Mon Sep 17 00:00:00 2001 From: Pontus Lundin Date: Mon, 5 May 2025 09:48:52 +0200 Subject: [PATCH 2/6] add tests --- .../transform/schema-object/object.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/openapi-typescript/test/transform/schema-object/object.test.ts b/packages/openapi-typescript/test/transform/schema-object/object.test.ts index 062f734a5..769b450d2 100644 --- a/packages/openapi-typescript/test/transform/schema-object/object.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/object.test.ts @@ -99,6 +99,51 @@ describe("transformSchemaObject > object", () => { // options: DEFAULT_OPTIONS, }, ], + [ + "patternProperties > empty object", + { + given: { type: "object", patternProperties: {} }, + want: `{ + [key: string]: unknown; +}`, + }, + ], + [ + "patternProperties > basic", + { + given: { type: "object", patternProperties: { "^a": { type: "string" } } }, + want: `{ + [key: string]: string; +}`, + }, + ], + [ + "patternProperties > enum", + { + given: { type: "object", patternProperties: { "^a": { type: "string", enum: ["a", "b", "c"] } } }, + want: `{ + [key: string]: "a" | "b" | "c"; +}`, + }, + ], + [ + "patternProperties > multiple patterns", + { + given: { type: "object", patternProperties: { "^a": { type: "string" }, "^b": { type: "number" } } }, + want: `{ + [key: string]: string | number; +}`, + }, + ], + [ + "patternProperties > additional and patterns", + { + given: { type: "object", additionalProperties: { type: "number" }, patternProperties: { "^a": { type: "string" } } }, + want: `{ + [key: string]: number | string; +}`, + }, + ], [ "nullable", { From b2723a37f2779a8062497329f5846f0b6825ec60 Mon Sep 17 00:00:00 2001 From: Pontus Lundin Date: Mon, 5 May 2025 10:26:08 +0200 Subject: [PATCH 3/6] fix fmt issue in test file --- .../test/transform/schema-object/object.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/openapi-typescript/test/transform/schema-object/object.test.ts b/packages/openapi-typescript/test/transform/schema-object/object.test.ts index 769b450d2..4aa275e80 100644 --- a/packages/openapi-typescript/test/transform/schema-object/object.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/object.test.ts @@ -138,7 +138,11 @@ describe("transformSchemaObject > object", () => { [ "patternProperties > additional and patterns", { - given: { type: "object", additionalProperties: { type: "number" }, patternProperties: { "^a": { type: "string" } } }, + given: { + type: "object", + additionalProperties: { type: "number" }, + patternProperties: { "^a": { type: "string" } }, + }, want: `{ [key: string]: number | string; }`, From 70f8d8dd0e508173d6fc525f95d2cd92dd22d02e Mon Sep 17 00:00:00 2001 From: Pontus Lundin Date: Mon, 5 May 2025 10:30:43 +0200 Subject: [PATCH 4/6] add changeset --- .changeset/eleven-shoes-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eleven-shoes-mate.md diff --git a/.changeset/eleven-shoes-mate.md b/.changeset/eleven-shoes-mate.md new file mode 100644 index 000000000..bb1468886 --- /dev/null +++ b/.changeset/eleven-shoes-mate.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": minor +--- + +Add support for patternProperties From 50ad58635f0b99ce1e2aa3482cdfa76c37288c63 Mon Sep 17 00:00:00 2001 From: Pontus Lundin Date: Tue, 19 Aug 2025 10:28:03 +0200 Subject: [PATCH 5/6] update after feedback --- .../src/transform/schema-object.ts | 11 ++++- .../transform/schema-object/object.test.ts | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index b52cd8c90..b2b8225bb 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -547,18 +547,27 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor if (schemaObject.additionalProperties || options.ctx.additionalProperties || schemaObject.patternProperties) { const hasExplicitAdditionalProperties = typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length; + const hasImplicitAdditionalProperties = + schemaObject.additionalProperties === true || (typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length === 0); const hasExplicitPatternProperties = typeof schemaObject.patternProperties === "object" && Object.keys(schemaObject.patternProperties).length; const addlTypes = []; if (hasExplicitAdditionalProperties) { addlTypes.push(transformSchemaObject(schemaObject.additionalProperties as SchemaObject, options)); } + if (hasImplicitAdditionalProperties || (!schemaObject.additionalProperties && options.ctx.additionalProperties)) { + addlTypes.push(UNKNOWN); + } if (hasExplicitPatternProperties) { for (const [_, v] of getEntries(schemaObject.patternProperties ?? {}, options.ctx)) { addlTypes.push(transformSchemaObject(v, options)); } } - const addlType = addlTypes.length === 0 ? UNKNOWN : tsUnion(addlTypes); + + if (addlTypes.length === 0) return; + + const addlType = tsUnion(addlTypes); + return tsIntersection([ ...(coreObjectType.length ? [ts.factory.createTypeLiteralNode(coreObjectType)] : []), ts.factory.createTypeLiteralNode([ diff --git a/packages/openapi-typescript/test/transform/schema-object/object.test.ts b/packages/openapi-typescript/test/transform/schema-object/object.test.ts index 4aa275e80..64adbb24b 100644 --- a/packages/openapi-typescript/test/transform/schema-object/object.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/object.test.ts @@ -101,11 +101,22 @@ describe("transformSchemaObject > object", () => { ], [ "patternProperties > empty object", + { + given: { type: "object", patternProperties: {} }, + want: `Record`, + }, + ], + [ + "patternProperties > empty object with options.additionalProperties=true", { given: { type: "object", patternProperties: {} }, want: `{ [key: string]: unknown; }`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_CTX, additionalProperties: true }, + } }, ], [ @@ -132,6 +143,19 @@ describe("transformSchemaObject > object", () => { given: { type: "object", patternProperties: { "^a": { type: "string" }, "^b": { type: "number" } } }, want: `{ [key: string]: string | number; +}`, + }, + ], + [ + "patternProperties > additional=true and patterns", + { + given: { + type: "object", + additionalProperties: true, + patternProperties: { "^a": { type: "string" } }, + }, + want: `{ + [key: string]: unknown | string; }`, }, ], @@ -148,6 +172,22 @@ describe("transformSchemaObject > object", () => { }`, }, ], + [ + "patternProperties > patterns with options.additionalProperties=true", + { + given: { + type: "object", + patternProperties: { "^a": { type: "string" } }, + }, + want: `{ + [key: string]: unknown | string; +}`, + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_CTX, additionalProperties: true }, + } + }, + ], [ "nullable", { From d6d0f0eb7ddcb83e3119e64cef85f3eccaaa15f1 Mon Sep 17 00:00:00 2001 From: Pontus Lundin Date: Tue, 19 Aug 2025 11:16:38 +0200 Subject: [PATCH 6/6] fix lint issues --- .../src/transform/schema-object.ts | 8 ++++++-- .../transform/schema-object/object.test.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index b2b8225bb..c25fc7889 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -548,7 +548,9 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor const hasExplicitAdditionalProperties = typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length; const hasImplicitAdditionalProperties = - schemaObject.additionalProperties === true || (typeof schemaObject.additionalProperties === "object" && Object.keys(schemaObject.additionalProperties).length === 0); + schemaObject.additionalProperties === true || + (typeof schemaObject.additionalProperties === "object" && + Object.keys(schemaObject.additionalProperties).length === 0); const hasExplicitPatternProperties = typeof schemaObject.patternProperties === "object" && Object.keys(schemaObject.patternProperties).length; const addlTypes = []; @@ -564,7 +566,9 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor } } - if (addlTypes.length === 0) return; + if (addlTypes.length === 0) { + return; + } const addlType = tsUnion(addlTypes); diff --git a/packages/openapi-typescript/test/transform/schema-object/object.test.ts b/packages/openapi-typescript/test/transform/schema-object/object.test.ts index 64adbb24b..7440ab789 100644 --- a/packages/openapi-typescript/test/transform/schema-object/object.test.ts +++ b/packages/openapi-typescript/test/transform/schema-object/object.test.ts @@ -103,7 +103,7 @@ describe("transformSchemaObject > object", () => { "patternProperties > empty object", { given: { type: "object", patternProperties: {} }, - want: `Record`, + want: "Record", }, ], [ @@ -113,10 +113,10 @@ describe("transformSchemaObject > object", () => { want: `{ [key: string]: unknown; }`, - options: { - ...DEFAULT_OPTIONS, - ctx: { ...DEFAULT_CTX, additionalProperties: true }, - } + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_CTX, additionalProperties: true }, + }, }, ], [ @@ -182,10 +182,10 @@ describe("transformSchemaObject > object", () => { want: `{ [key: string]: unknown | string; }`, - options: { - ...DEFAULT_OPTIONS, - ctx: { ...DEFAULT_CTX, additionalProperties: true }, - } + options: { + ...DEFAULT_OPTIONS, + ctx: { ...DEFAULT_CTX, additionalProperties: true }, + }, }, ], [