Repository navigation
RFC: Add Model Schemas to OpenApi Docs #157
Description
Activity
- changed the title
[-]Add Model Schemas to OpenApi Docs[/-][+]RFC: Add Model Schemas to OpenApi Docs[/+]on Sep 7, 2022 Hi @chuckstock. I have added this feature as a "Maybe" in our
v1.0.0roadmap (#91). Any ideas how you would like this API to look/behave?Initial thoughts 👇
// models.ts export const User = z.object({ id: z.string().uuid(), name: z.string(), }); // router.ts import { User } from './models'; export const appRouter = trpc.router().query('getUsers', { meta: { openapi: { ... } }, input: ..., output: z.array(User), resolve: ... }); // openapi.json.ts import { User } from './models'; import { appRouter } from './router'; const openapi = generateOpenApiDocument(appRouter, { ..., models: [User] });
cc @StefanTerdell.
Reacted by chuckstock, Woodson Delhia, Jonathan Rehem, Rehan van der Merwe and Tony XiaoReacted by Eduardo Rabelo, Jonathan Rehem and Tony Xiao- addedenhancementNew feature or requestNew feature or requesthelp wantedExtra attention is neededExtra attention is needed
on Sep 7, 2022 Yeah this is almost exactly what I was thinking and was potentially expecting to find when reviewing the documentation. I think that makes a lot of sense without adding really any extra boilerplate or overhead.
Another package allows you to extend
ZodObjectsand turn them into openapi components, here is an example from their npm packaage:import { extendApi, generateSchema } from '@anatine/zod-openapi'; const aZodExtendedSchema = extendApi( z.object({ uid: extendApi(z.string().nonempty(), { title: 'Unique ID', description: 'A UUID generated by the server', }), firstName: z.string().min(2), lastName: z.string().optional(), email: z.string().email(), phoneNumber: extendApi(z.string().min(10), { description: 'US Phone numbers only', example: '555-555-5555', }), }), { title: 'User', description: 'A user schema', } ); const myOpenApiSchema = generateSchema(aZodExtendedSchema);
This is another suggestion, although I think I prefer your original suggestion for the API.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hi, any updates on this?
I am also very interested in this, its the only open issue on the previous roadmap. Can this still be looked at for a future release?
Reacted by Kevin Traver, magick93, Andy, Tristan Cartledge, Niel Thiart, mndhvn, Thiago Passos and Daniel SimsFrom PR #417 :
I needed a way to integrate my OpenAPI-extended Zod schemas (using
@asteasolutions/zod-to-openapi) with the rest of the document generated bytrpc-openapi, so I threw this together in an afternoon.This implementation integrates a registry of Zod schemas into the final document, and also links return, parameter, and request body types of tRPC procedures with these schemas.
A minimal example of how this looks with
@asteasolutions/zod-to-openapi(although it should work with any solution):import { OpenAPIRegistry, OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; import { setZodComponentDefinitions, setZodComponentSchemaGenerator } from "trpc-openapi"; import { z } from "zod"; const registry = new OpenAPIRegistry(); registry.register("ComponentName", z.object({ // ... })) const generator = new OpenApiGeneratorV3(registry.definitions); // ~~~ FUNCTIONS FROM THE PR BELOW ~~~ // The definitions are used to map the types referenced in tRPC procedures setZodComponentDefinitions(registry.definitions.reduce((acc, d) => { if (d.type === 'schema') { acc[d.schema._def.openapi._internal.refId] = d.schema; } return acc; }, {} as { [key: string]: z.ZodType })); // The schema generator generates the actual components in the OpenAPI document setZodComponentSchemaGenerator(() => generator.generateDocument(config).components?.schemas ?? {});I'm a bit late on the ball here haha. GHs UI sure is one of the UIs of all time
Dont know much about open api but I believe it moves the reused schemas out of what would be the idiomatic JSON schema position ("#/definitions" or "#/$defs"). But that's what the definitionPath option is for. It still presumes the root path to be the schema root though, but you can get around that with basePath.
Here's a basic example:
const baz = z.object({}) zodToJsonSchema(z.object({ x: baz }), { basePath: ["foo"], definitionPath: "bar", definitions: { baz }, })
>
{ type: 'object', properties: { x: { '$ref': 'foo/bar/baz' } }, required: [ 'x' ], additionalProperties: false, bar: { baz: { type: 'object', properties: {}, additionalProperties: false } } }
You should be able to just move out the definitions object ("bar" in the example) from the resulting schema to put wherever you want in the open api doc as long as you can know the path beforehand and point at it with definitionsPath
Actually here's a very rough proof of concept @jlalmes
import { zodToJsonSchema } from "zodToJsonSchema"; import { ZodSchema, z } from "zod"; function buildDefinition( paths: Record< string, Record< "get" | "post", { summary: string; responses: Record<number, ZodSchema<any>> } > >, schemas: Record<string, ZodSchema<any>>, ) { const result: any = { openapi: "3.0.0", paths: {}, components: { schemas: {}, }, }; for (const path in paths) { result.paths[path] ??= {}; for (const method in paths[path]) { result.paths[path][method] ??= { summary: paths[path][method].summary }; for (const response in paths[path][method].responses) { result.paths[path][method].responses ??= {}; const zodSchema = paths[path][method].responses[response]; const jsonSchema = zodToJsonSchema(zodSchema, { target: "openApi3", basePath: ["#", "components"], definitionPath: "schemas", definitions: schemas, }); result.components.schemas = { ...result.components.schemas, ...(jsonSchema as any).schemas, }; delete (jsonSchema as any).schemas; result.paths[path][method].responses[response] = jsonSchema; } } } return result; } // Component to be reused const todoSchema = z.object({ id: z.string().uuid(), content: z.string(), completed: z.boolean().optional(), }); // The final openapi.json const result = buildDefinition( { todos: { get: { summary: "Get a todo", responses: { 200: todoSchema } }, post: { summary: "Post a todo", responses: { 201: todoSchema } }, }, }, { todo: todoSchema }, ); console.dir(result, { depth: null }); const expectResult = { openapi: "6.6.6", paths: { todos: { get: { $ref: "#/components/schemas/todo" }, post: { $ref: "#/components/schemas/todo" }, }, }, components: { schemas: { todo: { type: "object", properties: { id: { type: "string", format: "uuid" }, content: { type: "string" }, completed: { type: "boolean" }, }, required: ["id", "content"], additionalProperties: false, }, }, }, };
I needed support for this also, published a quick implementation: https://www.npmjs.com/package/@devidw/trpc-openapi
Source-Code: https://github.com/devidw/trpc-openapi
All you need to do is pass a object with your model schemas when you generate the document:
import { generateOpenApiDocument } from "trpc-openapi"; import { z } from "zod"; const SomeSchema = z.object({ a: z.literal(69), }); generateOpenApiDocument(appRouter, { // ... defs: { SomeSchema, }, });
Reacted by Stefan Terdell, Michael Vanantwerp and Maxim ShammasovReacted by Maxim Shammasov
First, I'm loving the project so far and I really appreciate all the work you've done already.
I honestly don't mind the output requirement on a query/mutation, however, it would be nice to be able to define Model Schema's and reference those for the outputs inside the docs. (ex. Pet Model from Redoc).