Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

RFC: Add Model Schemas to OpenApi Docs #157

Description

@chuckstock

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).

Activity

  1. changed the title [-]Add Model Schemas to OpenApi Docs[/-] [+]RFC: Add Model Schemas to OpenApi Docs[/+] on Sep 7, 2022
  2. jlalmes commented on Sep 7, 2022

    @jlalmes
    Contributor

    Hi @chuckstock. I have added this feature as a "Maybe" in our v1.0.0 roadmap (#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.

  3. chuckstock commented on Sep 7, 2022

    @chuckstock
    Author

    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 ZodObjects and 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.

  4. stale commented on Nov 6, 2022

    @stale

    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.

  5. OscBacon commented on Jul 10, 2023

    @OscBacon

    Hi, any updates on this?

  6. rehanvdm commented on Aug 31, 2023

    @rehanvdm

    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?

  7. mndhvn commented on Oct 26, 2023

    @mndhvn

    From 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 by trpc-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 ?? {});
    
  8. StefanTerdell commented on Jan 16, 2024

    @StefanTerdell

    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

  9. StefanTerdell commented on Jan 19, 2024

    @StefanTerdell

    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,
          },
        },
      },
    };
  10. devidw commented on Jun 27, 2024

    @devidw

    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,
      },
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions