Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Add Model Schemas to OpenApi Docs #157

Open
chuckstock opened this issue Sep 7, 2022 · 8 comments
Open

RFC: Add Model Schemas to OpenApi Docs #157

chuckstock opened this issue Sep 7, 2022 · 8 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@chuckstock
Copy link

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

@jlalmes jlalmes changed the title Add Model Schemas to OpenApi Docs RFC: Add Model Schemas to OpenApi Docs Sep 7, 2022
@jlalmes
Copy link
Owner

jlalmes commented Sep 7, 2022

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.

@jlalmes jlalmes added enhancement New feature or request help wanted Extra attention is needed labels Sep 7, 2022
@jlalmes jlalmes mentioned this issue Sep 7, 2022
24 tasks
@chuckstock
Copy link
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.

@stale
Copy link

stale bot commented Nov 6, 2022

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.

@OscBacon
Copy link

Hi, any updates on this?

@rehanvdm
Copy link

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?

@calasanmarko
Copy link

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 ?? {});

@StefanTerdell
Copy link

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

@StefanTerdell
Copy link

StefanTerdell commented Jan 19, 2024

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,
      },
    },
  },
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

6 participants