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

Make use of $ref in JSON Schema #34

Closed
ghost opened this issue Sep 8, 2020 · 5 comments
Closed

Make use of $ref in JSON Schema #34

ghost opened this issue Sep 8, 2020 · 5 comments

Comments

@ghost
Copy link

ghost commented Sep 8, 2020

const Person = Type.Object({
  'name': Type.String(),
  'age': Type.Number()
});
const ChessMatch = Type.Object({
  'playerWhite': Person,
  'playerBlack': Person
});
console.log(JSON.stringify(ChessMatch));

outputs

{
  "type": "object",
  "properties": {
    "playerWhite": {
      "type": "object",
      "properties": {
        "name": {"type": "string"},
        "age": {"type": "number"}
      },
      "required": ["name","age"]
    },
    "playerBlack": {
      "type": "object",
      "properties": // same as above
// ...

This means, the definition of Person is repeated in the output. Instead, please consider using the $ref keyword. The output would then look something like this:

{
  "type": "object",
  "definitions": {
    "person": {
      "type": "object",
      "properties": {
        "name": {"type": "string"},
        "age": {"type": "number"}
      }
    }
  },
  "properties": {
    "playerWhite": {"$ref": "#/definitions/person"},
    "playerBlack": {"$ref": "#/definitions/person"}
  }
}
@sinclairzx81
Copy link
Owner

@Remirror-zz Hi

I like the idea, however there isn't any way to trivially resolve the "definitions"."person" property or the {"$ref": "#/definitions/person"} as the name "person" isn't directly resolvable from const Person = Type.Object({ ... }). TypeBox would need to be extended to support the feature.

One option may be to extend TypeBox to support something like...

const Person = Type.Object({
  'name': Type.String(),
  'age': Type.Number()
}, { ref: 'person' }); // new

const ChessMatch = Type.Object({
  'playerWhite': Person, // check here if Person has a 'ref', and if so assign it to "definitions"
  'playerBlack': Person
});

The only reservation I'd have about implementing this is it may add a fair amount of complexity to the current schema generation. But id be curious to see an implementation.

Would you be interested in having a go implementing a reference design for this feature? I guess the make or break depends on just how intuitive the schema generation turns out to be for $ref types.

Let me know!

@ghost
Copy link
Author

ghost commented Sep 9, 2020

Good point, but I think that using $ref is possible even without specifying additional properties for a type such as Person, at least if the name of this type is not important. In this case, whenever the generator recognizes a schema is used more than once (except for primitive/trivial schemas of course), it can be made a definition and referenced by $ref. However, I would agree that this may rather be an approach of schema optimization (in terms of length) and too implicit for the actual use case. So for that, I like your design suggestion.

Additional difficulties will arise though if one wants to harness the actual power of $ref by including definitions of already existing schemas. This would require some serious design thinking, which should probably be done from the beginning.

For my case, I have now decided to go the other way round and put the priority on JSON schema rather than on TypeScript and thus to create my schema files manually and have them converted to interfaces by approaches such as json-schema-to-typescript and validated by ajv, so I fear I cannot be of help for you at the moment.

@sinclairzx81
Copy link
Owner

@Remirror-zz

Cool, thanks for the suggestion mate, its a good one. Ill leave this issue up for a while and give the feature some thought. TypeBox is due for some updates to align with some new features in TS 4.0, so may give this some thought then when I get around to those updates.

Thanks again!

@mooyoul
Copy link
Contributor

mooyoul commented Sep 28, 2020

I want this functionality too. But The problem is - TypeBox doesn't know which schema is to be exported. You'll have to create your own "Schema Container" (or "Schema Storage"), traverse entire definitions, and replace "referenced" schemas to JSONSchema reference object ($ref). I've done this manually in our framework (see serverless-seoul/corgi@f817e42#diff-eda32f3dbf6b5eb9924a1cef12c90cf6R194-R222)

@sinclairzx81
Copy link
Owner

sinclairzx81 commented Jul 6, 2021

@Remirror-zz Hi. This issue has been outstanding for a while. I have recently pushed an update to TypeBox to support $ref. You can read about the feature here https://github.com/sinclairzx81/typebox#reference-types. (note, this is still in an experimental state).

Note that $ref Reference Types currently don't support referencing into the same schema (as per your initial example) (as it's quite difficult for TypeBox to flatten all declarations when composing for large nested schemas). So all referenced schemas MUST live inside a Box type that can be referenced into.

As such, your original example can be expressed as.

const Person = Type.Object({ name: Type.String(), age: Type.Number() })

const Common = Type.Box('https://chess.app.com/common', { 
    Person 
})

const ChessMatch = Type.Object({
  playerWhite: Type.Ref(Common, 'Person'),
  playerBlack: Type.Ref(Common, 'Person'),
});

This produces the following two schemas.

const Common = {
  "$id": "https://chess.app.com/common",
  "definitions": {
    "Person": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "age": {
          "type": "number"
        }
      },
      "required": [
        "name",
        "age"
      ]
    }
  }
}
const ChessMatch  = {
  "type": "object",
  "properties": {
    "playerWhite": {
      "$ref": "https://chess.app.com/common#/definitions/Person"
    },
    "playerBlack": {
      "$ref": "https://chess.app.com/common#/definitions/Person"
    }
  },
  "required": [
    "playerWhite",
    "playerBlack"
  ]
}

For validation using AJV, you would now pass the Common referenceable schema to AJV via the addSchema() function. This allows the ChessMatch schema to validate against the loaded Common schema containing the Person type. As follows.

const ajv = new Ajv()
ajv.addSchema(Common) // adds the `Person` definition to AJV

ajv.validate(ChessMatch, {
   playerWhite: { name: 'dave', age: 42 },
   playerBlack: { name: 'alice', age: 28 },
})

Note that this feature provides a form of namespacing for common related schemas. It's this direction I feel yields the most downstream benefit (in terms of practicality). Going with this approach, it would be possible to manually merge in the schemas into a single schema (as a manual downstream process), but it's not something that TypeBox should necessarily handle as part of its API (as there are a number of ways achieve this). Also, going with split schemas also promotes the idea of defining domain objects that can be loaded as groups (and suggests dependency on domains at a namespace level).

Will close off this issue, but feel free to re-open if you have additional thoughts on the current implementation.
Cheers
S

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants