From b8ffbbd386e7fdb93dcbde027caaa446a7b844ab Mon Sep 17 00:00:00 2001 From: loup Date: Tue, 2 Dec 2025 14:59:54 +0100 Subject: [PATCH] fix(openapi): use ZModel AST array flag for TypeDef[] @json fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a model field uses `TypeDef[] @json` directly (e.g., `title TranslatedField[] @json`), the OpenAPI generator was not generating the correct array schema. Instead of: ```json "title": { "type": "array", "items": { "$ref": "#/components/schemas/TranslatedField" } } ``` It was generating: ```json "title": { "$ref": "#/components/schemas/TranslatedField" } ``` The bug was that `def.isList` comes from Prisma's DMMF, but Prisma treats all `@json` fields as plain `Json` type and doesn't know about the `[]` array notation in ZModel. The array information is only available in the ZModel AST via `field.type.array`. This fix uses `field.type.array` from the ZModel AST instead of `def.isList` from DMMF when generating OpenAPI schemas for TypeDef-referenced Json fields. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/plugins/openapi/src/rpc-generator.ts | 4 +- .../plugins/openapi/tests/openapi-rpc.test.ts | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index 22a19e133..463d3411e 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -785,9 +785,11 @@ export class RPCOpenAPIGenerator extends OpenAPIGeneratorBase { const field = dataModel.fields.find((f) => f.name === def.name); if (field?.type.reference?.ref && isTypeDef(field.type.reference.ref)) { // This Json field references a TypeDef + // Use field.type.array from ZModel AST instead of def.isList from DMMF, + // since Prisma treats TypeDef fields as plain Json and doesn't know about arrays return this.wrapArray( this.wrapNullable(this.ref(field.type.reference.ref.name, true), !def.isRequired), - def.isList + field.type.array ); } } diff --git a/packages/plugins/openapi/tests/openapi-rpc.test.ts b/packages/plugins/openapi/tests/openapi-rpc.test.ts index c61e01b1a..731b38f2a 100644 --- a/packages/plugins/openapi/tests/openapi-rpc.test.ts +++ b/packages/plugins/openapi/tests/openapi-rpc.test.ts @@ -518,6 +518,65 @@ model Product { } }); + it('array of TypeDef with enum directly on model field', async () => { + for (const specVersion of ['3.0.0', '3.1.0']) { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${normalizePath(path.resolve(__dirname, '../dist'))}' + specVersion = '${specVersion}' +} + +enum Language { + FR + EN + ES + DE + IT +} + +type TranslatedField { + language Language + content String +} + +model Article { + id String @id @default(cuid()) + title TranslatedField[] @json + description TranslatedField[] @json + + @@allow('all', true) +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + await OpenAPIParser.validate(output); + + const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); + expect(parsed.openapi).toBe(specVersion); + + // Verify TranslatedField TypeDef is generated + expect(parsed.components.schemas.TranslatedField).toBeDefined(); + + // Verify Language enum is generated + expect(parsed.components.schemas.Language).toBeDefined(); + + // Verify enum reference inside TranslatedField + expect(parsed.components.schemas.TranslatedField.properties.language.$ref).toBe('#/components/schemas/Language'); + + // Verify array of TypeDef directly on model field + expect(parsed.components.schemas.Article.properties.title.type).toBe('array'); + expect(parsed.components.schemas.Article.properties.title.items.$ref).toBe('#/components/schemas/TranslatedField'); + + // Verify second array field as well + expect(parsed.components.schemas.Article.properties.description.type).toBe('array'); + expect(parsed.components.schemas.Article.properties.description.items.$ref).toBe('#/components/schemas/TranslatedField'); + } + }); + it('full-text search', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` generator js {