Skip to content

Conversation

kerwanp
Copy link

@kerwanp kerwanp commented Jul 5, 2025

πŸ”— Linked issue

❓ Type of change

  • ✨ 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.

@thetutlage
Copy link
Contributor

Hello @kerwanp

The high-level approach looks fine to me. However, I will have a few changes.

  • I will rename the json property on a rule to toJSONSchema. Which indicates, this method will be called when converting the Vine schema node to a JSON schema node.
  • Similarly, the protected method on the Vine schema classes should be called toJSONSchema.
  • Finally, the meta property should not be strictly tied to JSON schema. If we want to do it, we should rename it to toJSONSchema. 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.

@thetutlage
Copy link
Contributor

Also, we should write some tests for the vine.group and vine.union schema types as well πŸ‘

@kerwanp
Copy link
Author

kerwanp commented Jul 7, 2025

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

@thetutlage
Copy link
Contributor

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?

@kerwanp
Copy link
Author

kerwanp commented Jul 13, 2025

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

@thetutlage
Copy link
Contributor

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.

@thetutlage
Copy link
Contributor

@kerwanp What are the next steps for this PR? Do you need any feedback from me?

@kerwanp
Copy link
Author

kerwanp commented Jul 31, 2025

@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

@kerwanp
Copy link
Author

kerwanp commented Aug 27, 2025

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",
  }

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

Successfully merging this pull request may close these issues.

2 participants