Add json-schema support
🔗 Linked issue
❓ Type of change
- [x] ✨ New feature (a non-breaking change that adds functionality)
📚 Description
This feature adds the ability to transform validators to json-schema using the toJSONSchema method.
This will open a lot of doors for libraries that can build around the json-schema standards (openapi generation, form builder, configuration files, etc).
Implementation
This is currently a draft and serves as an implementation proposal.
The json-schema is generated during the schema validation, allowing the user to retrieve it using toJSONSchema.
const validator = vine.compile(vine.object({
hello: vine.string(),
}))
const schema = validator.toJSONSchema()
assert.toDeepEqual(schema, {
type: 'object',
properties: {
hello: { type: 'string' }
},
required: ['hello']
})
All ConstructableSchema[PARSE] method must also return the a JSONSchemaV7. This is used to build the root of the schema. (e.g https://github.com/kerwanp/vine/blob/4.x/src/schema/base/literal.ts#L583-L583)
When creating rules, it is possible to pass an optional parameter for altering the json-schema.
export const emailRule = createRule<EmailOptions | undefined>(
function email(value, options, field) {
if (!helpers.isEmail(value as string, options)) {
field.report(messages.email, 'email', field)
}
},
{
json: (schema) => {
schema.format = 'email'
},
}
)
The user is able to provide custom meta that will be merged with the schema to provide custom parameters.
vine.string().meta({ description: "Hello world" })
📝 Checklist
- [ ] I have linked an issue or discussion.
- [ ] I have updated the documentation accordingly.
Hello @kerwanp
The high-level approach looks fine to me. However, I will have a few changes.
- I will rename the
jsonproperty on a rule totoJSONSchema. Which indicates, this method will be called when converting the Vine schema node to a JSON schema node. - Similarly, the
protectedmethod on the Vine schema classes should be calledtoJSONSchema. - Finally, the
metaproperty should not be strictly tied to JSON schema. If we want to do it, we should rename it totoJSONSchema. It will accept either a JSON schema object, or a function.
Also, once we merge this PR, Vine will support JSON schema as a first-class citizen and all custom schema types must implement the toJSONSchema method as-well.
Also, we should write some tests for the vine.group and vine.union schema types as well 👍
Thanks for the feedback!
I added support for vine.group, vine.literal, vine.union and vine.unionOfTypes.
Concerning the meta method. We can either change this to be more generic allowing to retrieve the provided meta when using toJSON() which would be also merged with the json-schema or we could make it specific to json schema (with a different method name). What do you prefer?
I also started the implementation of integration tests that validate values against the generate json-schema using ajv. This can help use spot issues with the complexity of some generated schemas (tuples, records, etc). What do you think?
I we are good with the implementation I will start expanding the test cases, cleaning up my work and add some comments
I also started the implementation of integration tests that validate values against the generate json-schema using ajv. This can help use spot issues with the complexity of some generated schemas (tuples, records, etc). What do you think?
Yeah, this seems like a great strategy.
Concerning the meta method. We can either change this to be more generic allowing to retrieve the provided meta when using toJSON() which would be also merged with the json-schema or we could make it specific to json schema (with a different method name). What do you prefer?
I will be okay with a more generic approach. However, the issue is, how do we know which properties to merge with the JSON schema?
I will be okay with a more generic approach. However, the issue is, how do we know which properties to merge with the JSON schema?
LIbraries like Zod provide a meta method that accept anything with a set of known properties that will be merged (title, description, examples, etc). This set is really restrictive and does not really allow schema customization. We can for the moment just rely on JSONSchema as it covers most of the use case. We could rename the method to json or jsonSchema and if metadata is a asked feature the current implementation could be extended
We could rename the method to json or jsonSchema and if metadata is a asked feature the current implementation could be extended
Cool, let's go with that. Maybe people can rely on the JSONschema properties for their other needs as well.
@kerwanp What are the next steps for this PR? Do you need any feedback from me?
@kerwanp What are the next steps for this PR? Do you need any feedback from me?
Hi, I am currently in vacations, will be back the 8th to finish this PR and I think I have all the information required
I've implemented the different integration tests, it helped me identify a lot of different edge cases.
I will now start updating the existing tests as some of them now contain the JSON schema (286 of them...).
We could either avoid using deep equal or simply bring the json schema in the expected results.
Also it seems that the reference ids are now calculated in different orders with my changes, what is the impact?
Object {
"conditions": Array [
Object {
- "conditionalFnRefId": "ref://2",
+ "conditionalFnRefId": "ref://1",
"schema": Object {
"allowNull": true,
"bail": true,
"fieldName": "*",
"isOptional": false,
+ "jsonSchema": Object {
+ "type": "null",
+ },
"parseFnId": undefined,
"propertyName": "*",
"subtype": "null",
"type": "literal",
"validations": Array [],
},
},
Object {
- "conditionalFnRefId": "ref://3",
+ "conditionalFnRefId": "ref://2",
"schema": Object {
"allowNull": false,
"bail": true,
"fieldName": "*",
"isOptional": false,
+ "jsonSchema": Object {
+ "enum": Array [
+ "1",
+ 1,
+ "true",
+ true,
+ "on",
+ "0",
+ 0,
+ "false",
+ false,
+ ],
+ },
"parseFnId": undefined,
"propertyName": "*",
"subtype": "boolean",
"type": "literal",
"validations": Array [
Object {
"implicit": false,
"isAsync": false,
"name": "boolean",
- "ruleFnId": "ref://4",
+ "ruleFnId": "ref://3",
},
],
},
},
Object {
- "conditionalFnRefId": "ref://5",
+ "conditionalFnRefId": "ref://4",
"schema": Object {
"allowNull": false,
"bail": true,
- "dataTypeValidatorFnId": "ref://6",
+ "dataTypeValidatorFnId": "ref://5",
"fieldName": "*",
"isOptional": false,
+ "jsonSchema": Object {
+ "type": "string",
+ },
"parseFnId": undefined,
"propertyName": "*",
"subtype": "string",
"type": "literal",
"validations": Array [],
},
},
],
- "elseConditionalFnRefId": "ref://1",
+ "elseConditionalFnRefId": "ref://6",
"fieldName": "*",
+ "jsonSchema": Object {
+ "anyOf": Array [
+ Object {
+ "type": "null",
+ },
+ Object {
+ "enum": Array [
+ "1",
+ 1,
+ "true",
+ true,
+ "on",
+ "0",
+ 0,
+ "false",
+ false,
+ ],
+ },
+ Object {
+ "type": "string",
+ },
+ ],
+ },
"propertyName": "*",
"type": "union",
}