"""Full JSON Schema validation for workflow input/output values. The engine used to check only ``required`` keys; everything else (wrong type, unknown enum value, nested ``properties``/``items`` violations) slipped through until downstream nodes exploded with confusing errors. This module runs the graph's ``inputSchema`` / ``outputSchema`` through ``Draft202012Validator`` and returns structured, frontend-friendly errors (path + message) instead of leaking Python tracebacks. Kept as pure functions so both the node executors (``WorkflowError``) and the HTTP router (400 responses) can share the exact same rules. """ from __future__ import annotations from typing import Any from jsonschema import Draft202012Validator # Cap the reported errors so a wildly invalid payload cannot produce a # multi-megabyte error body. _MAX_ERRORS = 20 def schema_issues(value: Any, schema: dict[str, Any] | None) -> list[dict[str, Any]]: """Validate ``value`` against ``schema``. Returns ``[]`` when the schema is empty/absent (nothing declared = nothing enforced). Each issue is ``{"path": [...], "message": str}`` with ``path`` pointing at the offending field, e.g. ``["options", 0, "name"]``. """ if not isinstance(schema, dict) or not schema: return [] try: validator = Draft202012Validator(schema) # Construction alone does not check the schema — malformed keywords # (e.g. a non-string ``pattern``) only explode during validation. errors = sorted(validator.iter_errors(value), key=lambda err: list(err.absolute_path)) except Exception: # noqa: BLE001 - a malformed user schema must not 500 return [{"path": [], "message": "schema 定义本身无效,无法用于校验"}] out: list[dict[str, Any]] = [] seen: set[tuple[Any, ...]] = set() for err in errors: path = [str(p) if not isinstance(p, int) else p for p in err.absolute_path] key = tuple(path) if key in seen: continue seen.add(key) out.append({"path": path, "message": _humanize(err)}) if len(out) >= _MAX_ERRORS: out.append({"path": [], "message": f"……其余问题已省略(共 {len(errors)} 处)"}) break return out def _humanize(err: Any) -> str: """Map the common jsonschema phrasings to short Chinese messages.""" message = str(err.message) validator = getattr(err, "validator", "") path = ".".join(str(p) for p in err.absolute_path) or "输入" if validator == "required": missing = message.split("'")[1] if "'" in message else "?" return f"{path} 缺少必填字段:{missing}" if validator == "enum": return f"{path} 的值不在允许范围内:{message}" if validator == "type": return f"{path} 类型不正确:{message}" if validator == "additionalProperties": return f"{path} 含有未声明的额外字段:{message}" return f"{path} 不符合 schema:{message}" __all__ = ["schema_issues"]