From 37975319f288c1cbc4f500d9d13309cb2cfa4797 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:15:32 +0800 Subject: [PATCH] feat: Add customized json schema validation (#25408) --- .../error-message.tsx | 2 +- .../components/workflow/nodes/llm/utils.ts | 200 ++------------ web/pnpm-lock.yaml | 2 +- web/utils/draft-07.json | 245 ++++++++++++++++++ web/utils/validators.ts | 27 ++ 5 files changed, 289 insertions(+), 187 deletions(-) create mode 100644 web/utils/draft-07.json create mode 100644 web/utils/validators.ts diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message.tsx index c21aa1405e..6e8a2b2fad 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message.tsx @@ -17,7 +17,7 @@ const ErrorMessage: FC = ({ className, )}> -
+
{message}
diff --git a/web/app/components/workflow/nodes/llm/utils.ts b/web/app/components/workflow/nodes/llm/utils.ts index 045acf3993..7f13998cd7 100644 --- a/web/app/components/workflow/nodes/llm/utils.ts +++ b/web/app/components/workflow/nodes/llm/utils.ts @@ -1,9 +1,8 @@ +import { z } from 'zod' import { ArrayType, Type } from './types' import type { ArrayItems, Field, LLMNodeType } from './types' -import type { Schema, ValidationError } from 'jsonschema' -import { Validator } from 'jsonschema' -import produce from 'immer' -import { z } from 'zod' +import { draft07Validator, forbidBooleanProperties } from '@/utils/validators' +import type { ValidationError } from 'jsonschema' export const checkNodeValid = (_payload: LLMNodeType) => { return true @@ -14,7 +13,7 @@ export const getFieldType = (field: Field) => { if (type !== Type.array || !items) return type - return ArrayType[items.type] + return ArrayType[items.type as keyof typeof ArrayType] } export const getHasChildren = (schema: Field) => { @@ -115,191 +114,22 @@ export const findPropertyWithPath = (target: any, path: string[]) => { return current } -const draft07MetaSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - $id: 'http://json-schema.org/draft-07/schema#', - title: 'Core schema meta-schema', - definitions: { - schemaArray: { - type: 'array', - minItems: 1, - items: { $ref: '#' }, - }, - nonNegativeInteger: { - type: 'integer', - minimum: 0, - }, - nonNegativeIntegerDefault0: { - allOf: [ - { $ref: '#/definitions/nonNegativeInteger' }, - { default: 0 }, - ], - }, - simpleTypes: { - enum: [ - 'array', - 'boolean', - 'integer', - 'null', - 'number', - 'object', - 'string', - ], - }, - stringArray: { - type: 'array', - items: { type: 'string' }, - uniqueItems: true, - default: [], - }, - }, - type: ['object', 'boolean'], - properties: { - $id: { - type: 'string', - format: 'uri-reference', - }, - $schema: { - type: 'string', - format: 'uri', - }, - $ref: { - type: 'string', - format: 'uri-reference', - }, - title: { - type: 'string', - }, - description: { - type: 'string', - }, - default: true, - readOnly: { - type: 'boolean', - default: false, - }, - examples: { - type: 'array', - items: true, - }, - multipleOf: { - type: 'number', - exclusiveMinimum: 0, - }, - maximum: { - type: 'number', - }, - exclusiveMaximum: { - type: 'number', - }, - minimum: { - type: 'number', - }, - exclusiveMinimum: { - type: 'number', - }, - maxLength: { $ref: '#/definitions/nonNegativeInteger' }, - minLength: { $ref: '#/definitions/nonNegativeIntegerDefault0' }, - pattern: { - type: 'string', - format: 'regex', - }, - additionalItems: { $ref: '#' }, - items: { - anyOf: [ - { $ref: '#' }, - { $ref: '#/definitions/schemaArray' }, - ], - default: true, - }, - maxItems: { $ref: '#/definitions/nonNegativeInteger' }, - minItems: { $ref: '#/definitions/nonNegativeIntegerDefault0' }, - uniqueItems: { - type: 'boolean', - default: false, - }, - contains: { $ref: '#' }, - maxProperties: { $ref: '#/definitions/nonNegativeInteger' }, - minProperties: { $ref: '#/definitions/nonNegativeIntegerDefault0' }, - required: { $ref: '#/definitions/stringArray' }, - additionalProperties: { $ref: '#' }, - definitions: { - type: 'object', - additionalProperties: { $ref: '#' }, - default: {}, - }, - properties: { - type: 'object', - additionalProperties: { $ref: '#' }, - default: {}, - }, - patternProperties: { - type: 'object', - additionalProperties: { $ref: '#' }, - propertyNames: { format: 'regex' }, - default: {}, - }, - dependencies: { - type: 'object', - additionalProperties: { - anyOf: [ - { $ref: '#' }, - { $ref: '#/definitions/stringArray' }, - ], - }, - }, - propertyNames: { $ref: '#' }, - const: true, - enum: { - type: 'array', - items: true, - minItems: 1, - uniqueItems: true, - }, - type: { - anyOf: [ - { $ref: '#/definitions/simpleTypes' }, - { - type: 'array', - items: { $ref: '#/definitions/simpleTypes' }, - minItems: 1, - uniqueItems: true, - }, - ], - }, - format: { type: 'string' }, - allOf: { $ref: '#/definitions/schemaArray' }, - anyOf: { $ref: '#/definitions/schemaArray' }, - oneOf: { $ref: '#/definitions/schemaArray' }, - not: { $ref: '#' }, - }, - default: true, -} as unknown as Schema - -const validator = new Validator() - export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => { - const schema = produce(schemaToValidate, (draft: any) => { - // Make sure the schema has the $schema property for draft-07 - if (!draft.$schema) - draft.$schema = 'http://json-schema.org/draft-07/schema#' - }) + // First check against Draft-07 + const result = draft07Validator(schemaToValidate) + // Then apply custom rule + const customErrors = forbidBooleanProperties(schemaToValidate) - const result = validator.validate(schema, draft07MetaSchema, { - nestedErrors: true, - throwError: false, - }) - - // Access errors from the validation result - const errors = result.valid ? [] : result.errors || [] - - return errors + return [...result.errors, ...customErrors] } -export const getValidationErrorMessage = (errors: ValidationError[]) => { +export const getValidationErrorMessage = (errors: Array) => { const message = errors.map((error) => { - return `Error: ${error.path.join('.')} ${error.message} Details: ${JSON.stringify(error.stack)}` - }).join('; ') + if (typeof error === 'string') + return error + else + return `Error: ${error.stack}\n` + }).join('') return message } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 694b7fb2da..c815ecb5e7 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -12603,7 +12603,7 @@ snapshots: '@vue/compiler-sfc@3.5.17': dependencies: - '@babel/parser': 7.28.0 + '@babel/parser': 7.28.3 '@vue/compiler-core': 3.5.17 '@vue/compiler-dom': 3.5.17 '@vue/compiler-ssr': 3.5.17 diff --git a/web/utils/draft-07.json b/web/utils/draft-07.json new file mode 100644 index 0000000000..99389d7ab4 --- /dev/null +++ b/web/utils/draft-07.json @@ -0,0 +1,245 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#" + } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/nonNegativeInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [] + } + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minLength": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "$ref": "#" + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": true + }, + "maxItems": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minItems": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { + "$ref": "#" + }, + "maxProperties": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minProperties": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "additionalProperties": { + "$ref": "#" + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "propertyNames": { + "format": "regex" + }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + } + }, + "propertyNames": { + "$ref": "#" + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { + "type": "string" + }, + "contentMediaType": { + "type": "string" + }, + "contentEncoding": { + "type": "string" + }, + "if": { + "$ref": "#" + }, + "then": { + "$ref": "#" + }, + "else": { + "$ref": "#" + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "not": { + "$ref": "#" + } + }, + "default": true +} diff --git a/web/utils/validators.ts b/web/utils/validators.ts new file mode 100644 index 0000000000..51b47feddf --- /dev/null +++ b/web/utils/validators.ts @@ -0,0 +1,27 @@ +import type { Schema } from 'jsonschema' +import { Validator } from 'jsonschema' +import draft07Schema from './draft-07.json' + +const validator = new Validator() + +export const draft07Validator = (schema: any) => { + return validator.validate(schema, draft07Schema as unknown as Schema) +} + +export const forbidBooleanProperties = (schema: any, path: string[] = []): string[] => { + let errors: string[] = [] + + if (schema && typeof schema === 'object' && schema.properties) { + for (const [key, val] of Object.entries(schema.properties)) { + if (typeof val === 'boolean') { + errors.push( + `Error: Property '${[...path, key].join('.')}' must not be a boolean schema`, + ) + } + else if (typeof val === 'object') { + errors = errors.concat(forbidBooleanProperties(val, [...path, key])) + } + } + } + return errors +}