sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.76k stars 152 forks source link

feature request: support references in openapi 3.0 spec #117

Closed pharindoko closed 2 years ago

pharindoko commented 2 years ago

Hey @sinclairzx81

getting following error trying to use fastify-swagger with openapi 3.0 and schema/refs

I do have following code in place:


import { Static, TLiteral, TUnion, Type } from "@sinclair/typebox";

type IntoStringUnion<T> = {
  [K in keyof T]: T[K] extends string ? TLiteral<T[K]> : never;
};

function StringUnion<T extends string[]>(
  values: [...T]
): TUnion<IntoStringUnion<T>> {
  return { enum: values } as any;
}

const T = StringUnion(["A", "B", "C"]);

type T = Static<typeof T>;

export const User = Type.Box({
  UserObject: Type.Object({
    name: Type.String(),
    mail: Type.Optional(Type.String({ format: "email" })),
    foo: Type.Optional(Type.String(T)),
  }),
});
const UserRef = Type.Ref(User, "UserObject");
export type UserType = Static<typeof UserRef>;
FastifyError [Error]: Failed building the validation schema for POST: /, due to error can't resolve reference User#/definitions/UserObject from id #
    at Boot.<anonymous> (backend/fastify-test/node_modules/fastify/lib/route.js:309:19)
    at Object.onceWrapper (events.js:519:28)
    at Boot.emit (events.js:412:35)
    at backend/fastify-test/node_modules/avvio/boot.js:153:12
    at backend/fastify-test/node_modules/avvio/plugin.js:270:7
    at done (backend/fastify-test/node_modules/avvio/plugin.js:202:5)
    at check (backend/fastify-test/node_modules/avvio/plugin.js:226:9)
    at internal/process/task_queues.js:141:7
    at AsyncResource.runInAsyncScope (async_hooks.js:197:9)
    at AsyncResource.runMicrotask (internal/process/task_queues.js:138:8) {
  code: 'FST_ERR_SCH_VALIDATION_BUILD',
  statusCode: 500
}

for openapi 3.0 spec the object schema has to be under --> components --> schemas

currently 'definitions' will be set (swagger 2.0) statically. https://github.com/sinclairzx81/typebox/blob/4a57e05116c765317f31bb13d767365ad4af7da6/src/typebox.ts#L499

My Request:

Can I have an enum or flag to use (swagger /'definitions' or openapi behaviour ('components/schemas') ? e.g.

export const UserRef = Type.Ref(User, "UserObject", Spec.Openapi);

or maybe with a string

export const UserRef = Type.Ref(User, "UserObject", "components/schemas");
sinclairzx81 commented 2 years ago

@pharindoko Hi, just to confirm, are you adding the Type.Box() to fastify's AJV implementation (usually via .addSchema()) ? The error seems to suggest that AJV can't resolve the referenced schema.

Failed building the validation schema ... can't resolve reference User#/definitions/UserObject from id 

You could try give the Type.Box() an $id. This can help AJV locate the correct schema.

export const User = Type.Box({
  UserObject: Type.Object({
    name: Type.String(),
    mail: Type.Optional(Type.String({ format: "email" })),
    foo:  Type.Optional(Type.String(T)),
  }),
}, { $id: 'Entities' }) // Required

... 

fastify.addSchema(Box)  // Required for target to be referenced.

const UserRef = Type.Ref(User, "UserObject")
export type UserType = Static<typeof UserRef>

As for adding a mechanism to override the default /definitions placement for Type.Box(...). I'd need to get more insight into that. Generally TypeBox tries to align with JSON schema conventions over Open API (in fact /definitions is planned to be renamed to /$defs in future releases), so providing mechanisms to work outside standard conventions is something I'm a little bit reluctant to do at this stage.

Would you be able to provide the expected Type.Box({...}) schema required Open API 3.0? I'm happy to take a look, noting that both Type.Box() and Type.Ref() are flagged as experimental features of TypeBox, so this may provide insights into how they can be improved.

S

pharindoko commented 2 years ago

Thanks for your reply @sinclairzx81

still not working - to give you an update ....

this fails at fastify.addSchema() call in index.ts

I get following error:

user: {"definitions":{"UserObject":{"$id":"UserObject","type":"object","properties":{"name":{"type":"string"},"mail":{"format":"email","type":"string"},"foo":{"enum":["A","B","C"],"type":"string"}},"required":["name"]}}}
...
FastifyError [Error]: Failed building the validation schema for POST: /, due to error schema is invalid: data.properties['kind'] should be object,boolean

index.ts

import fastify from "fastify";
import * as user from "./modules/user/user.controller";
import fastifySwagger from "fastify-swagger";
import helmet from "fastify-helmet";
import fastifySensible from "fastify-sensible";
import { User } from "./modules/user/user.schema";

const server = fastify({
  logger: true,
});
console.log("user: " + JSON.stringify(User));
User.$id = "UserObject";
server.addSchema(User.definitions.UserObject);

user.schema.ts

import { Static, TLiteral, TUnion, Type } from "@sinclair/typebox";

type IntoStringUnion<T> = {
  [K in keyof T]: T[K] extends string ? TLiteral<T[K]> : never;
};

function StringUnion<T extends string[]>(
  values: [...T]
): TUnion<IntoStringUnion<T>> {
  return { enum: values } as any;
}

const T = StringUnion(["A", "B", "C"]);

type T = Static<typeof T>;

export const User = Type.Box({
  UserObject: Type.Object(
    {
      name: Type.String(),
      mail: Type.Optional(Type.String({ format: "email" })),
      foo: Type.Optional(Type.String(T)),
    },
    { $id: "UserObject" }
  ),
});
const UserRef = Type.Ref(User, "UserObject");
export type UserType = Static<typeof UserRef>;
pharindoko commented 2 years ago

hmm this is what happens in ajv in background ...

schema is invalid: data.properties['kind'] should be object,boolean

image
sinclairzx81 commented 2 years ago

@pharindoko Hiya. Can you replace

server.addSchema(User.definitions.UserObject) // This isn't correct

// with

server.addSchema(User) // You should pass the Box directly to AJV

Additionally, the box should take an $id also which is used to namespace the schemas within.

export const User = Type.Box({
  UserObject: Type.Object(
    {
      name: Type.String(),
      mail: Type.Optional(Type.String({ format: "email" })),
      foo: Type.Optional(Type.String(T)),
    },
    { $id: "UserObject" }
  ),
}, { $id: 'User' }); // add here

This is required for Type.Ref(User, "UserObject") to target the correct schema when referencing into a Box.

Also, is there a need for the UserObject to exist with a Box? Box's are really intended to namespace a set of domain related schemas (similar to xmlns namespacing). If you only need a single type, you can use.

const UserObject = Type.Object({
  name: Type.String(), 
  mail: Type.Optional(Type.String({ format: "email" })),
  foo: Type.Optional(Type.String(T)),
}, { $id: "UserObject" })

server.addSchema(UserObject)

const R = Type.Ref(UserObject) // const R = { $ref: 'UserObject' }

As for the kind errors, be mindful that AJV 7+ runs in strict mode which will disallow the TypeBox kind and modifier keywords. There is documentation on configuring AJV here to enable these, alternatively, you can use Type.Strict(T) to cull out these keywords if you need to test things, refer to readme for more info.

pharindoko commented 2 years ago

Hey @sinclairzx81,

thanks for your amazing support! Yes I don`t need the Type.Box type. It is as you described it ... there was no need for me to change anything related to kind or modifier for ajv.

This is what I achieved so far.

image

Now the issue I have to solve is why I get def-0 as schema name.. This seems to be a fastify-swagger issue when fastify.addSchema is used... But it`s annoying cause I wanted to create my typescript interfaces for the frontend out of it.

Thanks a lot again for this amazing lib.

br,

pharindoko

pharindoko commented 2 years ago

I`ve added a repo with the solution https://github.com/pharindoko/fastify-prisma-ts

Would be great if type.ref would be a stable feature (not experimental)

sinclairzx81 commented 2 years ago

@pharindoko Hey thanks :)

Just on Type.Ref() and Type.Box() as experimental. The only external changes I expect there is potentially renaming Type.Box() to Type.Namespace() (this to align JSON Schema closer to XML's xmlns for a reference model on how boxed types should work, as well as to align with TypeScript's namespace keyword)

All other changes are anticipated to be internal to TypeBox (for example renaming definitions to $def). I am also currently waiting on some external functionality in AJV related to Type.Rec() recursive types, which has some flow of implications to Type.Ref().

Thanks for the reference project, very helpful.

baughmann commented 6 months ago

@pharindoko Hey thanks :)

Just on Type.Ref() and Type.Box() as experimental. The only external changes I expect there is potentially renaming Type.Box() to Type.Namespace() (this to align JSON Schema closer to XML's xmlns for a reference model on how boxed types should work, as well as to align with TypeScript's namespace keyword)

All other changes are anticipated to be internal to TypeBox (for example renaming definitions to $def). I am also currently waiting on some external functionality in AJV related to Type.Rec() recursive types, which has some flow of implications to Type.Ref().

Thanks for the reference project, very helpful.

What did Type.Boxend up getting renamed to? I don't see anything like it on 0.32.14