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.
AFAIK there are possible 2 approaches
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
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:
~ typescript
t.type({
lat: t.number,
lon: t.number
}).description('Geographical coordinates (WGS84 format)')
~
Would you be interested in supporting such descriptions in io-ts?
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
})
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.
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
Most helpful comment
I really like the approach of using an
io-tsruntime 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:~ typescriptt.type({
lat: t.number,
lon: t.number
}).description('Geographical coordinates (WGS84 format)')
~
Would you be interested in supporting such descriptions in
io-ts?