diff --git a/api_schemas/README.md b/api_schemas/README.md new file mode 100644 index 000000000000..d5396523680e --- /dev/null +++ b/api_schemas/README.md @@ -0,0 +1,252 @@ +# ComfyUI Prompt API JSON Schema + +This directory contains JSON Schema definitions for the ComfyUI API, providing formal specification, validation, and IDE support for API integrations. + +## 📁 Files + +- **`prompt_format.json`** - JSON Schema for the `/prompt` endpoint request format +- **`validation.py`** - Python utilities for schema validation +- **`README.md`** - This documentation file + +## 🚀 Quick Start + +### 1. Get the Schema + +The JSON Schema is available at: `GET /schema/prompt` or `GET /api/schema/prompt` + +```bash +curl http://localhost:8188/schema/prompt +``` + +### 2. Enable Validation (Optional) + +Add `?validate_schema=true` to your POST requests for server-side validation: + +```bash +curl -X POST http://localhost:8188/prompt?validate_schema=true \ + -H "Content-Type: application/json" \ + -d @your_prompt.json +``` + +### 3. IDE Setup + +Most modern IDEs support JSON Schema for autocomplete and validation: + +**VS Code:** +```json +{ + "json.schemas": [ + { + "fileMatch": ["**/comfyui_prompt*.json"], + "url": "http://localhost:8188/schema/prompt" + } + ] +} +``` + +**IntelliJ/PyCharm:** +Settings → Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings + +## 📋 Schema Overview + +The prompt format schema defines the structure for ComfyUI workflow execution requests: + +```json +{ + "prompt": { + "1": { + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "model.safetensors" + } + }, + "2": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "a beautiful landscape", + "clip": ["1", 1] + } + } + }, + "prompt_id": "optional-uuid", + "client_id": "optional-client-id" +} +``` + +### Key Properties + +- **`prompt`** (required) - The node graph defining the workflow +- **`prompt_id`** (optional) - Unique identifier for tracking execution +- **`number`** (optional) - Execution priority (lower = higher priority) +- **`front`** (optional) - If true, prioritize this execution +- **`extra_data`** (optional) - Additional metadata +- **`client_id`** (optional) - WebSocket client identifier +- **`partial_execution_targets`** (optional) - Array of specific nodes to execute + +### Node Structure + +Each node in the prompt is keyed by a numeric string ID and contains: + +- **`class_type`** (required) - The ComfyUI node class name +- **`inputs`** (required) - Input values or connections to other nodes +- **`_meta`** (optional) - Metadata not used in execution + +### Input Types + +Node inputs can be: + +1. **Direct values:** `"text": "hello world"` +2. **Node connections:** `"clip": ["1", 0]` (node_id, output_slot) + +## 🛠️ Validation + +### Python Integration + +```python +from api_schemas.validation import validate_prompt_format + +data = {"prompt": {...}} +is_valid, error_msg = validate_prompt_format(data) + +if not is_valid: + print(f"Validation failed: {error_msg}") +``` + +### Server-Side Validation + +Enable validation with query parameter: + +``` +POST /prompt?validate_schema=true +``` + +Returns `400 Bad Request` with detailed error information if validation fails. + +## 🔧 Development + +### Requirements + +For validation features: +```bash +pip install jsonschema +``` + +### Schema Updates + +When updating the schema: + +1. Modify `prompt_format.json` +2. Test with real ComfyUI workflows +3. Update examples and documentation +4. Verify backward compatibility + +### Testing + +```python +# Test schema loading +from api_schemas.validation import load_prompt_schema +schema = load_prompt_schema() +assert schema is not None + +# Test validation +from api_schemas.validation import validate_prompt_format +valid_prompt = {"prompt": {"1": {"class_type": "TestNode", "inputs": {}}}} +is_valid, error = validate_prompt_format(valid_prompt) +assert is_valid +``` + +## 📚 Examples + +### Basic Text-to-Image Workflow + +```json +{ + "prompt": { + "1": { + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "model.safetensors" + } + }, + "2": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "a beautiful landscape", + "clip": ["1", 1] + } + }, + "3": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "blurry, low quality", + "clip": ["1", 1] + } + }, + "4": { + "class_type": "KSampler", + "inputs": { + "seed": 12345, + "steps": 20, + "cfg": 7.0, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1.0, + "model": ["1", 0], + "positive": ["2", 0], + "negative": ["3", 0], + "latent_image": ["5", 0] + } + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + } + }, + "6": { + "class_type": "VAEDecode", + "inputs": { + "samples": ["4", 0], + "vae": ["1", 2] + } + }, + "7": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "ComfyUI", + "images": ["6", 0] + } + } + } +} +``` + +### Partial Execution + +```json +{ + "prompt": { + "1": {"class_type": "LoadImage", "inputs": {"image": "input.png"}}, + "2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0]}} + }, + "partial_execution_targets": ["2"], + "prompt_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +## 🤝 Contributing + +This schema implementation addresses GitHub issue [#8899](https://github.com/comfyanonymous/ComfyUI/issues/8899). + +To contribute: + +1. Test with real ComfyUI workflows +2. Report issues or inaccuracies +3. Suggest improvements for better IDE support +4. Help with documentation and examples + +## 📄 License + +This follows the same license as ComfyUI main repository. diff --git a/api_schemas/__init__.py b/api_schemas/__init__.py new file mode 100644 index 000000000000..f45439a27339 --- /dev/null +++ b/api_schemas/__init__.py @@ -0,0 +1,20 @@ +""" +ComfyUI API Schema validation package + +This package provides JSON Schema definitions and validation utilities +for the ComfyUI API endpoints. +""" + +from .validation import ( + validate_prompt_format, + load_prompt_schema, + get_schema_info, + JSONSCHEMA_AVAILABLE +) + +__all__ = [ + 'validate_prompt_format', + 'load_prompt_schema', + 'get_schema_info', + 'JSONSCHEMA_AVAILABLE' +] diff --git a/api_schemas/prompt_format.json b/api_schemas/prompt_format.json new file mode 100644 index 000000000000..74e47e39fcd6 --- /dev/null +++ b/api_schemas/prompt_format.json @@ -0,0 +1,205 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/comfyanonymous/ComfyUI/schemas/prompt_format.json", + "title": "ComfyUI Prompt API Format", + "description": "JSON Schema for the ComfyUI Prompt API format used with the /prompt endpoint", + "type": "object", + "properties": { + "prompt": { + "type": "object", + "description": "The node graph defining the workflow to execute", + "patternProperties": { + "^[0-9]+$": { + "type": "object", + "description": "Node definition with numeric ID as key", + "properties": { + "class_type": { + "type": "string", + "description": "The class name of the node (e.g., 'CheckpointLoaderSimple', 'KSampler')", + "minLength": 1 + }, + "inputs": { + "type": "object", + "description": "Input values or connections for this node", + "patternProperties": { + ".*": { + "oneOf": [ + { + "type": "array", + "description": "Connection to another node [node_id, output_slot]", + "prefixItems": [ + { + "type": "string", + "description": "ID of the source node", + "pattern": "^[0-9]+$" + }, + { + "type": "integer", + "description": "Output slot index of the source node", + "minimum": 0 + } + ], + "items": false, + "minItems": 2, + "maxItems": 2 + }, + { + "not": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "prefixItems": [ + {"type": "string", "pattern": "^[0-9]+$"}, + {"type": "integer", "minimum": 0} + ] + }, + "description": "Direct input value (string, number, boolean, object, or array)" + } + ] + } + }, + "additionalProperties": true + }, + "_meta": { + "type": "object", + "description": "Optional metadata for the node (not used in execution)", + "properties": { + "title": { + "type": "string", + "description": "Custom title for the node" + } + }, + "additionalProperties": true + } + }, + "required": ["class_type", "inputs"], + "additionalProperties": false + } + }, + "additionalProperties": false, + "minProperties": 1 + }, + "prompt_id": { + "type": "string", + "description": "Optional unique identifier for this prompt execution. If not provided, a UUID will be generated.", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$|^[a-fA-F0-9]{32}$" + }, + "number": { + "type": "number", + "description": "Optional execution priority number. Lower numbers execute first." + }, + "front": { + "type": "boolean", + "description": "If true, this prompt will be prioritized (given a negative number)." + }, + "extra_data": { + "type": "object", + "description": "Additional metadata and configuration for the execution", + "properties": { + "client_id": { + "type": "string", + "description": "Client identifier for WebSocket communication" + } + }, + "additionalProperties": true + }, + "client_id": { + "type": "string", + "description": "Client identifier for WebSocket communication (alternative to extra_data.client_id)" + }, + "partial_execution_targets": { + "type": "array", + "description": "Optional array of node IDs to execute selectively. If provided, only these nodes (and their dependencies) will be executed.", + "items": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Node ID to execute" + }, + "uniqueItems": true + } + }, + "required": ["prompt"], + "additionalProperties": false, + "examples": [ + { + "prompt": { + "1": { + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "model.safetensors" + } + }, + "2": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "a beautiful landscape", + "clip": ["1", 1] + } + }, + "3": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "blurry, low quality", + "clip": ["1", 1] + } + }, + "4": { + "class_type": "KSampler", + "inputs": { + "seed": 12345, + "steps": 20, + "cfg": 7.0, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1.0, + "model": ["1", 0], + "positive": ["2", 0], + "negative": ["3", 0], + "latent_image": ["5", 0] + } + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + } + }, + "6": { + "class_type": "VAEDecode", + "inputs": { + "samples": ["4", 0], + "vae": ["1", 2] + } + }, + "7": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "ComfyUI", + "images": ["6", 0] + } + } + } + }, + { + "prompt": { + "1": { + "class_type": "LoadImage", + "inputs": { + "image": "input.png" + } + }, + "2": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "output", + "images": ["1", 0] + } + } + }, + "prompt_id": "550e8400-e29b-41d4-a716-446655440000", + "client_id": "client_123" + } + ] +} diff --git a/api_schemas/validation.py b/api_schemas/validation.py new file mode 100644 index 000000000000..ae2645248073 --- /dev/null +++ b/api_schemas/validation.py @@ -0,0 +1,86 @@ +""" +JSON Schema validation utilities for ComfyUI API +""" +import json +import os +import logging +from typing import Dict, Any, Tuple, Optional + +try: + import jsonschema + from jsonschema import validate, ValidationError + JSONSCHEMA_AVAILABLE = True +except ImportError: + JSONSCHEMA_AVAILABLE = False + ValidationError = Exception # Fallback for type hints + + +def load_prompt_schema() -> Optional[Dict[str, Any]]: + """ + Load the prompt format JSON schema from file. + + Returns: + Dict containing the schema, or None if not found/invalid + """ + schema_path = os.path.join(os.path.dirname(__file__), "prompt_format.json") + try: + with open(schema_path, 'r', encoding='utf-8') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + logging.warning(f"Could not load prompt schema: {e}") + return None + + +def validate_prompt_format(data: Dict[str, Any], warn_only: bool = True) -> Tuple[bool, Optional[str]]: + """ + Validate prompt data against the JSON schema. + + Args: + data: The prompt data to validate + warn_only: If True, log warnings instead of raising errors + + Returns: + Tuple of (is_valid, error_message) + """ + if not JSONSCHEMA_AVAILABLE: + if warn_only: + logging.debug("jsonschema not available, skipping schema validation") + return True, None + + schema = load_prompt_schema() + if schema is None: + if warn_only: + logging.debug("Could not load schema, skipping validation") + return True, None + + try: + validate(instance=data, schema=schema) + return True, None + except ValidationError as e: + error_msg = f"Prompt format validation failed: {e.message}" + if e.path: + error_msg += f" at path: {'.'.join(str(p) for p in e.path)}" + + if warn_only: + logging.warning(f"Schema validation warning: {error_msg}") + return True, error_msg # Still return True for warnings + else: + return False, error_msg + + +def get_schema_info() -> Dict[str, Any]: + """ + Get information about the schema validation capability. + + Returns: + Dict containing schema validation status and info + """ + info = { + "jsonschema_available": JSONSCHEMA_AVAILABLE, + "schema_loaded": load_prompt_schema() is not None, + } + + if JSONSCHEMA_AVAILABLE: + info["jsonschema_version"] = getattr(jsonschema, "__version__", "unknown") + + return info diff --git a/server.py b/server.py index 0553a0dd79a0..2c98e004d7e8 100644 --- a/server.py +++ b/server.py @@ -584,7 +584,31 @@ async def system_stats(request): @routes.get("/features") async def get_features(request): - return web.json_response(feature_flags.get_server_features()) + features = feature_flags.get_server_features() + + try: + from api_schemas.validation import get_schema_info + features["schema_validation"] = get_schema_info() + except ImportError: + features["schema_validation"] = { + "jsonschema_available": False, + "schema_loaded": False + } + + return web.json_response(features) + + @routes.get("/schema/prompt") + async def get_prompt_schema(request): + """Serve the JSON Schema for the prompt API format""" + schema_path = os.path.join(os.path.dirname(__file__), "api_schemas", "prompt_format.json") + try: + with open(schema_path, 'r', encoding='utf-8') as f: + schema = json.load(f) + return web.json_response(schema) + except FileNotFoundError: + return web.json_response({"error": "Schema file not found"}, status=404) + except json.JSONDecodeError as e: + return web.json_response({"error": f"Invalid schema file: {str(e)}"}, status=500) @routes.get("/prompt") async def get_prompt(request): @@ -669,6 +693,26 @@ async def get_queue(request): async def post_prompt(request): logging.info("got prompt") json_data = await request.json() + + # Optional JSON Schema validation + validate_schema = request.rel_url.query.get('validate_schema', 'false').lower() == 'true' + if validate_schema: + try: + from api_schemas.validation import validate_prompt_format + is_valid, error_msg = validate_prompt_format(json_data, warn_only=False) + if not is_valid: + error = { + "type": "schema_validation_failed", + "message": "Request does not conform to prompt API schema", + "details": error_msg, + "extra_info": { + "schema_url": "/schema/prompt" + } + } + return web.json_response({"error": error, "node_errors": {}}, status=400) + except ImportError: + logging.warning("Schema validation requested but validation module not available") + json_data = self.trigger_on_prompt(json_data) if "number" in json_data: