APIDevTools / json-schema-ref-parser

Parse, Resolve, and Dereference JSON Schema $ref pointers in Node and browsers
https://apitools.dev/json-schema-ref-parser
MIT License
942 stars 226 forks source link

Possibility to add `onBundle` callback? #346

Open graynk opened 4 months ago

graynk commented 4 months ago

Hi! This is just an issue to get your opinion on adding a new onBundle callback as part of the options object, similar to onDereference. I'm willing to contribute this, if you're open to adding this feature. Here's some context on why we need it:

We use json-schema-ref-parser to parse and bundle OpenAPI and AsyncAPI files, heavily relying on overriding the resolvers to fetch external references from GitHub.

One issue with that is: if external $ref contains $schema and $id - the bundle() function keeps them, but the resulting OpenAPI document fails validation, due to unexpected additional properties. Here's a concrete minimal example:

schema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "someId",
  "type": "object",
  "properties": {
    "exampleProperty": {
      "description": "Lorem ipsum and so on",
      "type": "boolean"
    }
  }
}

api.yaml

openapi: "3.0.3"
info:
  title: Example API
  version: "1.0"
servers:
  - url: https://localhost:3000/v1
paths:
  /example:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: 'schema.json'

script.ts

import fs from 'node:fs/promises';
import $RefParser from '@apidevtools/json-schema-ref-parser';
import swaggerParser from '@apidevtools/swagger-parser';
import yaml from 'yaml';

const body = await fs.readFile('api.yaml', 'utf8');
const content = yaml.parse(body);
const result = await $RefParser.bundle(content, { resolve: { external: true } });
await fs.writeFile('bundledApi.yaml', yaml.stringify(result));
await swaggerParser.validate(result);

bundledApi.yaml

openapi: 3.0.3
info:
  title: Example API
  version: "1.0"
servers:
  - url: https://localhost:3000/v1
paths:
  /example:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $schema: http://json-schema.org/draft-07/schema#
                $id: someId
                type: object
                properties:
                  exampleProperty:
                    description: Lorem ipsum and so on
                    type: boolean

This fails validation with:

SyntaxError: Swagger schema validation failed.
  #/paths/~1example/get/responses/200/content/application~1json/schema must NOT have additional properties
  #/paths/~1example/get/responses/200/content/application~1json/schema must NOT have additional properties
  #/paths/~1example/get/responses/200/content/application~1json/schema must have required property '$ref'
  #/paths/~1example/get/responses/200/content/application~1json/schema must match exactly one schema in oneOf
  #/paths/~1example/get/responses/200 must have required property '$ref'
  #/paths/~1example/get/responses/200 must match exactly one schema in oneOf

Removing $schema and $id would make it pass the validation, however there doesn't seem to be an easy way of doing that. If we were calling dereference() instead of bundle() - the problem would be trivially solved with something like this:

const result = await $RefParser.dereference(content, {
  resolve: { external: true },
  dereference: {
    onDereference: (path: string, value: JSONSchemaObject) => {
      if ('$schema' in value) {
        delete value.$schema;
      }
    },
  },
});

Do you think adding the possibility to pass in an onBundle callback would make sense here? Or can our problem be solved in some other way? Appreciate any feedback.

jonluca commented 4 months ago

That sounds reasonable, we'd add the feature if you make a PR