fix: fix json object validate (#29840)

This commit is contained in:
wangxiaolei
2025-12-18 14:46:00 +08:00
committed by GitHub
parent 46c9a59a31
commit dd237f129d
4 changed files with 82 additions and 57 deletions

View File

@@ -1,3 +1,4 @@
import json
from collections.abc import Sequence from collections.abc import Sequence
from enum import StrEnum, auto from enum import StrEnum, auto
from typing import Any, Literal from typing import Any, Literal
@@ -120,7 +121,7 @@ class VariableEntity(BaseModel):
allowed_file_types: Sequence[FileType] | None = Field(default_factory=list) allowed_file_types: Sequence[FileType] | None = Field(default_factory=list)
allowed_file_extensions: Sequence[str] | None = Field(default_factory=list) allowed_file_extensions: Sequence[str] | None = Field(default_factory=list)
allowed_file_upload_methods: Sequence[FileTransferMethod] | None = Field(default_factory=list) allowed_file_upload_methods: Sequence[FileTransferMethod] | None = Field(default_factory=list)
json_schema: dict[str, Any] | None = Field(default=None) json_schema: str | None = Field(default=None)
@field_validator("description", mode="before") @field_validator("description", mode="before")
@classmethod @classmethod
@@ -134,11 +135,17 @@ class VariableEntity(BaseModel):
@field_validator("json_schema") @field_validator("json_schema")
@classmethod @classmethod
def validate_json_schema(cls, schema: dict[str, Any] | None) -> dict[str, Any] | None: def validate_json_schema(cls, schema: str | None) -> str | None:
if schema is None: if schema is None:
return None return None
try: try:
Draft7Validator.check_schema(schema) json_schema = json.loads(schema)
except json.JSONDecodeError:
raise ValueError(f"invalid json_schema value {schema}")
try:
Draft7Validator.check_schema(json_schema)
except SchemaError as e: except SchemaError as e:
raise ValueError(f"Invalid JSON schema: {e.message}") raise ValueError(f"Invalid JSON schema: {e.message}")
return schema return schema

View File

@@ -1,3 +1,4 @@
import json
from collections.abc import Generator, Mapping, Sequence from collections.abc import Generator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Union, final from typing import TYPE_CHECKING, Any, Union, final
@@ -175,6 +176,13 @@ class BaseAppGenerator:
value = True value = True
elif value == 0: elif value == 0:
value = False value = False
case VariableEntityType.JSON_OBJECT:
if not isinstance(value, str):
raise ValueError(f"{variable_entity.variable} in input form must be a string")
try:
json.loads(value)
except json.JSONDecodeError:
raise ValueError(f"{variable_entity.variable} in input form must be a valid JSON object")
case _: case _:
raise AssertionError("this statement should be unreachable.") raise AssertionError("this statement should be unreachable.")

View File

@@ -1,3 +1,4 @@
import json
from typing import Any from typing import Any
from jsonschema import Draft7Validator, ValidationError from jsonschema import Draft7Validator, ValidationError
@@ -42,15 +43,25 @@ class StartNode(Node[StartNodeData]):
if value is None and variable.required: if value is None and variable.required:
raise ValueError(f"{key} is required in input form") raise ValueError(f"{key} is required in input form")
if not isinstance(value, dict):
raise ValueError(f"{key} must be a JSON object")
schema = variable.json_schema schema = variable.json_schema
if not schema: if not schema:
continue continue
if not value:
continue
try: try:
Draft7Validator(schema).validate(value) json_schema = json.loads(schema)
except json.JSONDecodeError as e:
raise ValueError(f"{schema} must be a valid JSON object")
try:
json_value = json.loads(value)
except json.JSONDecodeError as e:
raise ValueError(f"{value} must be a valid JSON object")
try:
Draft7Validator(json_schema).validate(json_value)
except ValidationError as e: except ValidationError as e:
raise ValueError(f"JSON object for '{key}' does not match schema: {e.message}") raise ValueError(f"JSON object for '{key}' does not match schema: {e.message}")
node_inputs[key] = value node_inputs[key] = json_value

View File

@@ -1,3 +1,4 @@
import json
import time import time
import pytest import pytest
@@ -46,14 +47,16 @@ def make_start_node(user_inputs, variables):
def test_json_object_valid_schema(): def test_json_object_valid_schema():
schema = { schema = json.dumps(
"type": "object", {
"properties": { "type": "object",
"age": {"type": "number"}, "properties": {
"name": {"type": "string"}, "age": {"type": "number"},
}, "name": {"type": "string"},
"required": ["age"], },
} "required": ["age"],
}
)
variables = [ variables = [
VariableEntity( VariableEntity(
@@ -65,7 +68,7 @@ def test_json_object_valid_schema():
) )
] ]
user_inputs = {"profile": {"age": 20, "name": "Tom"}} user_inputs = {"profile": json.dumps({"age": 20, "name": "Tom"})}
node = make_start_node(user_inputs, variables) node = make_start_node(user_inputs, variables)
result = node._run() result = node._run()
@@ -74,12 +77,23 @@ def test_json_object_valid_schema():
def test_json_object_invalid_json_string(): def test_json_object_invalid_json_string():
schema = json.dumps(
{
"type": "object",
"properties": {
"age": {"type": "number"},
"name": {"type": "string"},
},
"required": ["age", "name"],
}
)
variables = [ variables = [
VariableEntity( VariableEntity(
variable="profile", variable="profile",
label="profile", label="profile",
type=VariableEntityType.JSON_OBJECT, type=VariableEntityType.JSON_OBJECT,
required=True, required=True,
json_schema=schema,
) )
] ]
@@ -88,38 +102,21 @@ def test_json_object_invalid_json_string():
node = make_start_node(user_inputs, variables) node = make_start_node(user_inputs, variables)
with pytest.raises(ValueError, match="profile must be a JSON object"): with pytest.raises(ValueError, match='{"age": 20, "name": "Tom" must be a valid JSON object'):
node._run()
@pytest.mark.parametrize("value", ["[1, 2, 3]", "123"])
def test_json_object_valid_json_but_not_object(value):
variables = [
VariableEntity(
variable="profile",
label="profile",
type=VariableEntityType.JSON_OBJECT,
required=True,
)
]
user_inputs = {"profile": value}
node = make_start_node(user_inputs, variables)
with pytest.raises(ValueError, match="profile must be a JSON object"):
node._run() node._run()
def test_json_object_does_not_match_schema(): def test_json_object_does_not_match_schema():
schema = { schema = json.dumps(
"type": "object", {
"properties": { "type": "object",
"age": {"type": "number"}, "properties": {
"name": {"type": "string"}, "age": {"type": "number"},
}, "name": {"type": "string"},
"required": ["age", "name"], },
} "required": ["age", "name"],
}
)
variables = [ variables = [
VariableEntity( VariableEntity(
@@ -132,7 +129,7 @@ def test_json_object_does_not_match_schema():
] ]
# age is a string, which violates the schema (expects number) # age is a string, which violates the schema (expects number)
user_inputs = {"profile": {"age": "twenty", "name": "Tom"}} user_inputs = {"profile": json.dumps({"age": "twenty", "name": "Tom"})}
node = make_start_node(user_inputs, variables) node = make_start_node(user_inputs, variables)
@@ -141,14 +138,16 @@ def test_json_object_does_not_match_schema():
def test_json_object_missing_required_schema_field(): def test_json_object_missing_required_schema_field():
schema = { schema = json.dumps(
"type": "object", {
"properties": { "type": "object",
"age": {"type": "number"}, "properties": {
"name": {"type": "string"}, "age": {"type": "number"},
}, "name": {"type": "string"},
"required": ["age", "name"], },
} "required": ["age", "name"],
}
)
variables = [ variables = [
VariableEntity( VariableEntity(
@@ -161,7 +160,7 @@ def test_json_object_missing_required_schema_field():
] ]
# Missing required field "name" # Missing required field "name"
user_inputs = {"profile": {"age": 20}} user_inputs = {"profile": json.dumps({"age": 20})}
node = make_start_node(user_inputs, variables) node = make_start_node(user_inputs, variables)
@@ -214,7 +213,7 @@ def test_json_object_optional_variable_not_provided():
variable="profile", variable="profile",
label="profile", label="profile",
type=VariableEntityType.JSON_OBJECT, type=VariableEntityType.JSON_OBJECT,
required=False, required=True,
) )
] ]
@@ -223,5 +222,5 @@ def test_json_object_optional_variable_not_provided():
node = make_start_node(user_inputs, variables) node = make_start_node(user_inputs, variables)
# Current implementation raises a validation error even when the variable is optional # Current implementation raises a validation error even when the variable is optional
with pytest.raises(ValueError, match="profile must be a JSON object"): with pytest.raises(ValueError, match="profile is required in input form"):
node._run() node._run()