io-ts icon indicating copy to clipboard operation
io-ts copied to clipboard

Convert to JSON Schema

Open xogeny opened this issue 7 years ago • 6 comments

I'm considering a use case where I'd like to export JSON Schema for interoperability with other tools or languages. Looking through the definition for Type, it seems like every instance defines a member named _tag. However, _tag is not part of Type. I realize that all these various types are never combined into a union type, so it might seem to be useless. But if I'd like to do this for some known subset of types, it seems useful to be able to access _tag in some type safe way.

The closest I seem to be able to come is to define a type like this:

type All = t.InterfaceType<any> | t.NumberType | ... /* all JS built-in types */;

In this case, _tag is public in all of these so All becomes a tagged union and I could switch on all the necessary cases and extract all the type information I need. Is this a reasonable approach? Is there a better one? BTW, I looked over #53 and, frankly, I could not understand most of what was being discussed in that thread.

Thanks.

xogeny avatar Aug 13 '18 18:08 xogeny

AFAIK there are possible 2 approaches

  • convert a runtime type to JSON Schema (either at runtime or compile time)
  • convert a JSON Schema to a runtime type (either at runtime or compile time)

In the first case (i.e. if your source of truth is a io-ts runtime type) I'd go for a tagged union like you said

A partial implementation

interface JSONSchemaArrayRuntimeType extends t.ArrayType<JSONSchemaRuntimeType> {}
type JSONSchemaObjectRuntimeType = t.InterfaceType<{ [key: string]: JSONSchemaRuntimeType }>
type JSONSchemaRuntimeType =
  | t.StringType
  | t.NumberType
  | t.BooleanType
  | JSONSchemaArrayRuntimeType
  | JSONSchemaObjectRuntimeType

export const toJSONSchema = (type: JSONSchemaRuntimeType): JSONSchema => {
  switch (type._tag) {
    case 'StringType':
      return { type: 'string' }
    case 'NumberType':
      return { type: 'number' }
    case 'BooleanType':
      return { type: 'boolean' }
    case 'ArrayType':
      return { type: 'array', item: toJSONSchema(type.type) }
    case 'InterfaceType':
      return {
        type: 'object',
        properties: {
          /* TODO */
        }
      }
  }
}

Note that you can try to prevent illegal conversions statically

toJSONSchema(
  t.type({
    foo: t.undefined
  })
) // static error

However conversions might become tricky if there are recursive runtime types involved.

The second option would be to go from a json schema to a runtime type, likely with code generation. In this case you may want to take a look at io-ts-codegen and specifically to this (partial) example

gcanti avatar Aug 14 '18 07:08 gcanti

I really like the approach of using an io-ts runtime type as the source of truth, but I would be missing a few features to cover my use case. Specifically, I need to be able to define a description for my types:

t.type({
  lat: t.number,
  lon: t.number
}).description('Geographical coordinates (WGS84 format)')

Would you be interested in supporting such descriptions in io-ts?

julienrf avatar Apr 25 '19 14:04 julienrf

I was looking for JSSchema myself too.

Description is a bit specific, but could there be a way to associate meta data?

t.type({
  lat: t.number,
  lon: t.number
}).metaData({ 
  description: 'Geographical coordinates (WGS84 format)', 
  name: "Coordinates" 
})

So it could contain anything, and be used by other iterators. I'm also looking a way to turn io-ts definitions to SQL table definitions, so meta data would be helpful there too.

Naturally it should work with numbers etc too, like

t.type({
  lat: t.number.metaData({ name: "Latitude", description: "..."}),
  lon: t.number
})

Ciantic avatar May 07 '19 11:05 Ciantic

I'm experimenting with converting JSON schemas to io-ts. I think it makes sense to do it this way since JSONSchema is simpler than io-ts validators which are turing complete. In other words it is relatively easy to check minLength in an io-ts refinement but it is nearly impossible to check an arbitrary refinement with JSON schema. Therefore it makes sense to describe the language independent part as a JSON schema and translate it to language specific validators.

See converter script, example input, example output

cyberixae avatar Oct 02 '19 13:10 cyberixae

I wrote a minimalistic CLI and moved the converter script into a separate package to make it a tiny bit more reusable. See https://www.npmjs.com/package/io-ts-from-json-schema

cyberixae avatar May 15 '20 15:05 cyberixae

NB. there's an example in the repo, which uses Schema to convert to JSON schema. It works wonderfully. However, it is using the older json-schema package, but it's easy to port to eg. openapi3-ts.

I'm wondering how one should add eg. the aforementioned description or examples fields? It's easy to do for primitives, but if you want to use it for more complex types, it gets harder. (The same goes for decoder's withMessage.)

ljani avatar Apr 12 '22 07:04 ljani