elysiajs / elysia-swagger

A plugin for Elysia to auto-generate Swagger page
MIT License
74 stars 36 forks source link

Unable to use local $ref components #93

Open Mudbill opened 5 months ago

Mudbill commented 5 months ago

I had a functional work-around for version 0.8.0, but 0.8.1 replaced the SwaggerUI with Scalar, which does not accept this work-around. I have tried many things, but been unable to find a working solution for referencing schema components that exist within the same OpenAPI file.

I specify the schemas with new Elysia().model({ User: UserModel })

This puts the UserModel into #/components/schemas/User in the OpenAPI file.

I would like to reference this model (or several) in a response or body, by wrapping it in an object, like so:

new Elysia()
  .model({ User: UserModel })
  .get("/users", () => {
    return { users: [] }
  }, {
    response: t.Object({
      users: t.Array(
        // Some reference to '#/components/schemas/User'
      )
    })
  })

Currently, the only way I've found to somewhat get this effect is to put the UserModel into the array, but this breaks the link to the existing schema. The end result is that the OpenAPI file duplicates the schema, inflating it exponentionally depending on how many times you wish to use it.

I want the file to appear like this

```json { "openapi": "3.1.0", "servers": [ { "url": "http://localhost:3000", "description": "Test server" } ], "components": { "schemas": { "User": { "description": "User", "additionalProperties": false, "type": "object", "properties": { "email": { "format": "email", "type": "string" }, "id": { "readOnly": true, "type": "integer" }, "name": { "type": "string" } }, "required": [ "email", "id", "name" ] } } }, "info": { "title": "Custom API", "description": "These are the custom docs", "version": "1.0.0" }, "paths": { "/users/": { "get": { "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "users": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } }, "required": [ "users" ] } } } } }, "operationId": "getUsers", "summary": "Get all users" } } } } ```

Instead it appears like this

```json { "openapi": "3.1.0", "servers": [ { "url": "http://localhost:3000", "description": "Test server" } ], "components": { "schemas": { "User": { "description": "User", "additionalProperties": false, "type": "object", "properties": { "email": { "format": "email", "type": "string" }, "id": { "readOnly": true, "type": "integer" }, "name": { "type": "string" } }, "required": [ "email", "id", "name" ] } } }, "info": { "title": "Custom API", "description": "These are the custom docs", "version": "1.0.0" }, "paths": { "/users/": { "get": { "responses": { "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "users": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "format": "email", "type": "string" } }, "required": [ "id", "name", "email" ] } } }, "required": [ "users" ] } } } } }, "operationId": "getUsers" } } } } ```

Some things I have tried

With SwaggerUI, I was able to get this to work with t.Array(t.Ref(UserModel)), however Scalar does not like this approach, instead attempting to fetch GET /User as it seems to think the schema is external to the file. I'm not sure I can fault Scalar for this though, as it seems the format was incorrect originally despite working in SwaggerUI.

To get it to work with SwaggerUI, the UserModel was required to specify the $id field. It seems by setting it to "User", the file ended up with "$ref": "User", and the local schema included the "$id": "User" field. I think the issue is that the $ref should be #/components/schemas/User, not just User. There may be other issues aside from this, but this is the most prominent issue I can spot. I've been reading the OpenAPI v3.1 spec, and I don't think it allows referring to local schemas simply by name, despite them having an $id, but I could be wrong. Either way, Scalar breaks with how it currently is.

Using:

Reproduction

Open to see

```ts const UserModel = t.Object( { id: t.Integer({ readOnly: true }), name: t.String(), email: t.String({ format: "email" }), }, { $id: "User", } ); const app = new Elysia() .use(swagger({ documentation: { openapi: "3.1.0" } })) .model({ User: UserModel }) .get("/users/", () => ({ users: [] }), { response: t.Object({ users: t.Array(t.Ref(UserModel)), }), type: "application/json" }); ``` Go to `/swagger/json` to get the resulting file (Scalar at `/swagger` just generates a blank page) The file looks like this: ```json { "openapi": "3.1.0", "info": { "title": "Elysia Documentation", "description": "Development documentation", "version": "0.0.0" }, "paths": { "/users/": { "get": { "responses": { "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "users": { "type": "array", "items": { "$ref": "User" } } }, "required": ["users"] } } } } }, "operationId": "getUsers" } } }, "components": { "schemas": { "User": { "$id": "User", "type": "object", "properties": { "id": { "readOnly": true, "type": "integer" }, "name": { "type": "string" }, "email": { "format": "email", "type": "string" } }, "required": ["id", "name", "email"] } } } } ``` If you put it into https://editor-next.swagger.io/ it renders more or less correctly If you put it into https://docs.scalar.com/swagger-editor you can see that it fails to locate the schema, falling back to null. Modify the `$ref` to be `#/components/schemas/User` and it will work.