En tant qu'ingénieur qui a passé plus de 3 000 heures à déboguer des intégrations d'API d'intelligence artificielle en production, je peux vous assurer d'une chose : le JSON Mode instable est le cauchemar n°1 des développeurs backend. Laissez-moi vous raconter une anecdote récente qui illustre parfaitement le problème.

Le scénario d'erreur réel qui m'a motivé à écrire cet article

Il y a trois semaines, l'un de nos clients — une startup fintech parisienne — a déployé un chatbot de support client basé sur l'extraction de données structurées. Tout fonctionnait parfaitement en phase de tests. Puis, en production, après exactement 847 requêtes réussies, boom :

Error: Unexpected token 'e', "error_code"... is not valid JSON
    at JSON.parse(<anonymous>)
    at parseResponse (/app/server.js:142:12)
    at processLLMResponse (/app/server.js:89:23)
    
Caused by: Model returned: "error_code: 429, message: Rate limit exceeded"

Puis, après correction de ce premier problème, un autre plus insidieux est apparu : le modèle renvoyait parfois du JSON valide mais avec des champs manquants, des valeurs null inattendues, ou pire encore — des tableaux malformés comme ["item1", "item2",] avec cette virgule traînante qui fait pleurer les parseurs JSON stricts.

Cet article est le fruit de 47 heures de recherche, de tests sur 6 providers différents, et de la compilation de toutes les solutions que j'ai validées en conditions réelles. Si vous utilisez le JSON Mode avec HolySheep AI ou toute autre API LLM, ce guide va vous sauver des nuits blanches.

Comprendre pourquoi le JSON Mode est instable par conception

Avant de plonger dans les solutions, il faut comprendre le problème fondamental. Les modèles de langage (LLMs) sont entraînés pour prédire le prochain token en fonction du contexte. Ils ne « calculent » pas du JSON comme un programme le ferait — ils génèrent du texte qui ressemble à du JSON.

Cette distinction est cruciale. Voici les 4 sources principales d'instabilité que j'ai identifiées :

Architecture de test : notre environnement de benchmark

Pour garantir des résultats reproductibles, voici le setup que j'utilise pour tous mes tests de stabilité JSON Mode :

#!/usr/bin/env python3
"""
Benchmark JSON Mode Stability - HolySheep AI
Test environment: 1000 requests, 3 different schemas
Metrics: success rate, parse time, token efficiency
"""

import asyncio
import json
import time
import httpx
from typing import Dict, List, Optional
from dataclasses import dataclass
import statistics

@dataclass
class JSONModeResult:
    request_id: str
    success: bool
    parse_time_ms: float
    token_count: int
    error: Optional[str] = None
    raw_response: Optional[str] = None

class HolySheepJSONBenchmark:
    BASE_URL = "https://api.holysheep.ai/v1"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.client = httpx.AsyncClient(
            timeout=30.0,
            limits=httpx.Limits(max_keepalive_connections=20)
        )
        self.results: List[JSONModeResult] = []
    
    async def query_structured(
        self, 
        prompt: str, 
        schema: Dict,
        model: str = "deepseek-v3.2"
    ) -> JSONModeResult:
        """Send request with JSON schema enforcement"""
        request_id = f"req_{int(time.time() * 1000)}"
        
        start_time = time.perf_counter()
        
        try:
            response = await self.client.post(
                f"{self.BASE_URL}/chat/completions",
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": model,
                    "messages": [{"role": "user", "content": prompt}],
                    "response_format": {
                        "type": "json_object",
                        "json_schema": schema
                    },
                    "max_tokens": 2000,
                    "temperature": 0.1  # Low temp for consistency
                }
            )
            
            parse_start = time.perf_counter()
            data = response.json()
            parse_time = (time.perf_counter() - parse_start) * 1000
            
            if "error" in data:
                return JSONModeResult(
                    request_id=request_id,
                    success=False,
                    parse_time_ms=parse_time,
                    token_count=0,
                    error=f"API Error: {data['error']}"
                )
            
            content = data["choices"][0]["message"]["content"]
            parsed = json.loads(content)  # This is where instability hits
            
            return JSONModeResult(
                request_id=request_id,
                success=True,
                parse_time_ms=parse_time,
                token_count=data["usage"]["total_tokens"],
                raw_response=content
            )
            
        except httpx.TimeoutException:
            return JSONModeResult(
                request_id=request_id,
                success=False,
                parse_time_ms=30000,
                token_count=0,
                error="Timeout - connection or read timeout"
            )
        except json.JSONDecodeError as e:
            return JSONModeResult(
                request_id=request_id,
                success=False,
                parse_time_ms=0,
                token_count=0,
                error=f"JSON Parse Error: {str(e)}"
            )
        except Exception as e:
            return JSONModeResult(
                request_id=request_id,
                success=False,
                parse_time_ms=0,
                token_count=0,
                error=f"Unexpected: {type(e).__name__}: {str(e)}"
            )

Example usage with real schema

async def main(): benchmark = HolySheepJSONBenchmark(api_key="YOUR_HOLYSHEEP_API_KEY") test_schema = { "name": "product_analysis", "strict": True, "schema": { "type": "object", "properties": { "product_name": {"type": "string"}, "price_eur": {"type": "number"}, "rating": {"type": "number", "minimum": 0, "maximum": 5}, "pros": {"type": "array", "items": {"type": "string"}}, "cons": {"type": "array", "items": {"type": "string"}} }, "required": ["product_name", "price_eur"] } } prompt = """Analyze this product and return structured data: iPhone 16 Pro - €1,229 - Rating 4.7/5 Best camera phone of 2025, but expensive and heavy.""" result = await benchmark.query_structured(prompt, test_schema) print(f"Success: {result.success}") print(f"Parse time: {result.parse_time_ms:.2f}ms") if result.success: print(f"Token count: {result.token_count}") if __name__ == "__main__": asyncio.run(main())

Solution 1 : Le schéma de validation avec fallback intelligent

La première ligne de défense contre l'instabilité du JSON Mode est une architecture de validation multicouche. Voici l'implémentation que je recommande, testée en production chez plusieurs de nos clients :

#!/usr/bin/env python3
"""
Robust JSON Mode Handler with validation and auto-retry
Includes: schema validation, partial parse recovery, automatic retry
"""

import json
import re
import time
import httpx
from typing import Any, Dict, Optional, List, Callable
from enum import Enum
from dataclasses import dataclass
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class ParseStrategy(Enum):
    STRICT = "strict"
    LENIENT = "lenient"
    RECOVERY = "recovery"

@dataclass
class ValidationResult:
    is_valid: bool
    errors: List[str]
    warnings: List[str]
    parsed_data: Optional[Dict] = None
    strategy_used: ParseStrategy = ParseStrategy.STRICT

class RobustJSONParser:
    """Handles unstable JSON Mode responses with multiple recovery strategies"""
    
    def __init__(self, required_fields: List[str], optional_fields: List[str] = None):
        self.required_fields = required_fields
        self.optional_fields = optional_fields or []
    
    def extract_json_from_text(self, text: str) -> Optional[str]:
        """Extract JSON block from potentially polluted response"""
        # Try direct parse first
        text = text.strip()
        
        # Strategy 1: JSON code blocks
        json_blocks = re.findall(r'``(?:json)?\s*([\s\S]*?)``', text)
        if json_blocks:
            return json_blocks[0].strip()
        
        # Strategy 2: Find first { and last }
        first_brace = text.find('{')
        last_brace = text.rfind('}')
        if first_brace != -1 and last_brace > first_brace:
            candidate = text[first_brace:last_brace + 1]
            # Validate it has balanced braces
            if self._balanced_braces(candidate):
                return candidate
        
        # Strategy 3: JSON array detection
        first_bracket = text.find('[')
        last_bracket = text.rfind(']')
        if first_bracket != -1 and last_bracket > first_bracket:
            candidate = text[first_bracket:last_bracket + 1]
            if self._balanced_brackets(candidate):
                return candidate
        
        return None
    
    def _balanced_braces(self, text: str) -> bool:
        """Check if JSON object has balanced braces"""
        count = 0
        for char in text:
            if char == '{':
                count += 1
            elif char == '}':
                count -= 1
            if count < 0:
                return False
        return count == 0
    
    def _balanced_brackets(self, text: str) -> bool:
        """Check if JSON array has balanced brackets"""
        count = 0
        for char in text:
            if char == '[':
                count += 1
            elif char == ']':
                count -= 1
            if count < 0:
                return False
        return count == 0
    
    def fix_common_issues(self, json_str: str) -> str:
        """Apply common fixes for malformed JSON"""
        # Fix trailing commas
        json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
        
        # Fix single quotes to double quotes
        json_str = re.sub(r"'([^']*)'", r'"\1"', json_str)
        
        # Fix unquoted keys (liberal JSON parsers sometimes accept this)
        # Note: This is risky and should be used carefully
        json_str = re.sub(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)', 
                          r'\1"\2"\3', json_str)
        
        # Fix missing quotes around string values
        json_str = re.sub(r':\s*([a-zA-Z][a-zA-Z0-9_\s]*)(\s*[,\}])', 
                          r': "\1"\2', json_str)
        
        return json_str
    
    def validate_against_schema(self, data: Dict) -> ValidationResult:
        """Validate parsed data against required schema"""
        errors = []
        warnings = []
        
        # Check required fields
        for field in self.required_fields:
            if field not in data:
                errors.append(f"Missing required field: {field}")
            elif data[field] is None or data[field] == "":
                errors.append(f"Field '{field}' is null or empty")
        
        # Check for unexpected fields (warning only)
        all_known_fields = set(self.required_fields + self.optional_fields)
        for key in data.keys():
            if key not in all_known_fields:
                warnings.append(f"Unexpected field: {key}")
        
        return ValidationResult(
            is_valid=len(errors) == 0,
            errors=errors,
            warnings=warnings,
            parsed_data=data
        )
    
    def parse_with_fallback(self, raw_response: str) -> ValidationResult:
        """Parse with multiple fallback strategies"""
        
        # Try extraction first
        json_str = self.extract_json_from_text(raw_response)
        if not json_str:
            return ValidationResult(
                is_valid=False,
                errors=["Could not extract JSON from response"],
                warnings=[]
            )
        
        # Strategy 1: Direct parse
        try:
            data = json.loads(json_str)
            return self.validate_against_schema(data)
        except json.JSONDecodeError:
            pass
        
        # Strategy 2: Fix common issues
        fixed = self.fix_common_issues(json_str)
        try:
            data = json.loads(fixed)
            result = self.validate_against_schema(data)
            result.strategy_used = ParseStrategy.RECOVERY
            return result
        except json.JSONDecodeError:
            pass
        
        # Strategy 3: Lenient regex-based extraction
        return self._regex_based_parse(raw_response)
    
    def _regex_based_parse(self, text: str) -> ValidationResult:
        """Last resort: extract data using regex patterns"""
        data = {}
        
        # Extract string values
        for field in self.required_fields + self.optional_fields:
            patterns = [
                rf'{field}[\s:]+["\']([^"\']+)["\']',
                rf'"{field}"\s*:\s*["\']([^"\']+)["\']',
                rf'{field}\s*:\s*([^\s,\}}]+)'
            ]
            for pattern in patterns:
                match = re.search(pattern, text, re.IGNORECASE)
                if match:
                    data[field] = match.group(1)
                    break
        
        if not data:
            return ValidationResult(
                is_valid=False,
                errors=["All parsing strategies failed"],
                warnings=[]
            )
        
        result = self.validate_against_schema(data)
        result.strategy_used = ParseStrategy.RECOVERY
        result.warnings.append("Parsed using fallback regex - data may be incomplete")
        return result


async def robust_json_mode_request(
    prompt: str,
    schema: Dict,
    api_key: str,
    max_retries: int = 3,
    timeout: float = 30.0
) -> Dict[str, Any]:
    """Complete request handler with automatic retry and validation"""
    
    parser = RobustJSONParser(
        required_fields=schema.get("required", []),
        optional_fields=list(schema.get("properties", {}).keys())
    )
    
    for attempt in range(max_retries):
        try:
            async with httpx.AsyncClient(timeout=timeout) as client:
                response = await client.post(
                    "https://api.holysheep.ai/v1/chat/completions",
                    headers={
                        "Authorization": f"Bearer {api_key}",
                        "Content-Type": "application/json"
                    },
                    json={
                        "model": "deepseek-v3.2",
                        "messages": [{"role": "user", "content": prompt}],
                        "response_format": {
                            "type": "json_object",
                            "json_schema": schema
                        },
                        "temperature": 0.1,
                        "max_tokens": 1500
                    }
                )
                
                data = response.json()
                
                if "error" in data:
                    logger.warning(f"Attempt {attempt + 1}: API error - {data['error']}")
                    if attempt < max_retries - 1:
                        await asyncio.sleep(2 ** attempt)  # Exponential backoff
                        continue
                    return {"success": False, "error": data["error"]}
                
                raw_content = data["choices"][0]["message"]["content"]
                result = parser.parse_with_fallback(raw_content)
                
                if result.is_valid:
                    return {
                        "success": True,
                        "data": result.parsed_data,
                        "parse_strategy": result.strategy_used.value,
                        "warnings": result.warnings
                    }
                else:
                    logger.warning(f"Attempt {attempt + 1}: Validation failed - {result.errors}")
                    
        except httpx.TimeoutException:
            logger.warning(f"Attempt {attempt + 1}: Timeout")
        except Exception as e:
            logger.error(f"Attempt {attempt + 1}: {type(e).__name__} - {str(e)}")
        
        if attempt < max_retries - 1:
            await asyncio.sleep(2 ** attempt)
    
    return {
        "success": False,
        "error": "All retry attempts failed after validation"
    }

Example with product extraction schema

if __name__ == "__main__": schema = { "type": "object", "properties": { "product_name": {"type": "string"}, "price_eur": {"type": "number"}, "in_stock": {"type": "boolean"}, "categories": {"type": "array", "items": {"type": "string"}} }, "required": ["product_name", "price_eur"] } import asyncio result = asyncio.run(robust_json_mode_request( prompt="Extract product info: Sony WH-1000XM5 headphones, €379, currently available, categories: audio, wireless, noise-cancelling", schema=schema, api_key="YOUR_HOLYSHEEP_API_KEY" )) print(json.dumps(result, indent=2))

Solution 2 : Gestion des erreurs réseau et timeouts avec rétention de contexte

Le deuxième axe d'instabilité vient du réseau lui-même. Voici une solution complète qui gère les déconnexions, les timeouts, et maintient un contexte cohérent même en cas d'échec :

#!/usr/bin/env python3
"""
Advanced JSON Mode handler with circuit breaker pattern
Handles: timeouts, rate limits, partial failures, context recovery
"""

import asyncio
import time
import httpx
from typing import Optional, Dict, Any, List
from dataclasses import dataclass, field
from enum import Enum
from collections import deque
import json

class CircuitState(Enum):
    CLOSED = "closed"      # Normal operation
    OPEN = "open"          # Failing, reject requests
    HALF_OPEN = "half_open"  # Testing if service recovered

@dataclass
class CircuitBreakerConfig:
    failure_threshold: int = 5
    recovery_timeout: float = 30.0
    half_open_max_calls: int = 3

@dataclass
class RequestAttempt:
    timestamp: float
    success: bool
    latency_ms: float
    error_type: Optional[str] = None

class CircuitBreaker:
    """Prevent cascading failures when API is unstable"""
    
    def __init__(self, config: CircuitBreakerConfig):
        self.config = config
        self.state = CircuitState.CLOSED
        self.failures = 0
        self.last_failure_time: Optional[float] = None
        self.half_open_calls = 0
        self.history: deque = field(default_factory=lambda: deque(maxlen=100))
    
    def can_execute(self) -> bool:
        if self.state == CircuitState.CLOSED:
            return True
        
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time >= self.config.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                self.half_open_calls = 0
                return True
            return False
        
        # HALF_OPEN
        if self.half_open_calls < self.config.half_open_max_calls:
            self.half_open_calls += 1
            return True
        return False
    
    def record_success(self):
        self.history.append(RequestAttempt(time.time(), True, 0))
        
        if self.state == CircuitState.HALF_OPEN:
            self.state = CircuitState.CLOSED
            self.failures = 0
    
    def record_failure(self, error_type: str, latency_ms: float = 0):
        self.history.append(RequestAttempt(time.time(), False, latency_ms, error_type))
        self.failures += 1
        self.last_failure_time = time.time()
        
        if self.state == CircuitState.HALF_OPEN:
            self.state = CircuitState.OPEN
        elif self.failures >= self.config.failure_threshold:
            self.state = CircuitState.OPEN
    
    def get_stats(self) -> Dict[str, Any]:
        if not self.history:
            return {"total_requests": 0, "success_rate": 100.0}
        
        total = len(self.history)
        successes = sum(1 for r in self.history if r.success)
        return {
            "total_requests": total,
            "success_rate": round((successes / total) * 100, 2),
            "state": self.state.value,
            "failures": self.failures
        }


class JSONModeResilientClient:
    """Production-ready client with all resilience patterns"""
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.holysheep.ai/v1"
        self.circuit_breaker = CircuitBreaker(CircuitBreakerConfig(
            failure_threshold=5,
            recovery_timeout=30.0
        ))
        self.session_context: List[Dict] = []
        self._client: Optional[httpx.AsyncClient] = None
    
    async def _get_client(self) -> httpx.AsyncClient:
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                timeout=httpx.Timeout(
                    connect=10.0,
                    read=45.0,
                    write=10.0,
                    pool=30.0
                ),
                limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
            )
        return self._client
    
    async def close(self):
        if self._client and not self._client.is_closed:
            await self._client.aclose()
    
    def _extract_json_safely(self, content: str) -> Optional[Dict]:
        """Extract and validate JSON from LLM response"""
        content = content.strip()
        
        # Remove markdown code blocks
        if content.startswith("```"):
            content = content.split("```")[1]
            if content.startswith("json"):
                content = content[4:]
            content = content.strip()
        
        # Find JSON boundaries
        start = content.find('{')
        end = content.rfind('}')
        
        if start != -1 and end != -1 and end > start:
            candidate = content[start:end + 1]
            try:
                return json.loads(candidate)
            except json.JSONDecodeError:
                pass
        
        # Try array format
        start = content.find('[')
        end = content.rfind(']')
        if start != -1 and end != -1 and end > start:
            candidate = content[start:end + 1]
            try:
                parsed = json.loads(candidate)
                if isinstance(parsed, list):
                    return {"items": parsed}
            except json.JSONDecodeError:
                pass
        
        return None
    
    async def structured_completion(
        self,
        prompt: str,
        schema: Dict,
        system_message: Optional[str] = None,
        max_retries: int = 3
    ) -> Dict[str, Any]:
        """
        Main entry point for JSON Mode requests with full resilience
        """
        if not self.circuit_breaker.can_execute():
            return {
                "success": False,
                "error": "Circuit breaker is OPEN - service temporarily unavailable",
                "circuit_stats": self.circuit_breaker.get_stats()
            }
        
        messages = []
        if system_message:
            messages.append({"role": "system", "content": system_message})
        messages.extend(self.session_context)
        messages.append({"role": "user", "content": prompt})
        
        for attempt in range(max_retries):
            start_time = time.time()
            
            try:
                client = await self._get_client()
                
                request_payload = {
                    "model": "deepseek-v3.2",
                    "messages": messages,
                    "response_format": {
                        "type": "json_object",
                        "json_schema": schema
                    },
                    "temperature": 0.1,
                    "max_tokens": 2000
                }
                
                response = await client.post(
                    f"{self.base_url}/chat/completions",
                    headers={
                        "Authorization": f"Bearer {self.api_key}",
                        "Content-Type": "application/json"
                    },
                    json=request_payload
                )
                
                latency_ms = (time.time() - start_time) * 1000
                
                if response.status_code == 429:
                    self.circuit_breaker.record_failure("rate_limit", latency_ms)
                    wait_time = int(response.headers.get("Retry-After", 60))
                    await asyncio.sleep(min(wait_time, 120))
                    continue
                
                if response.status_code >= 500:
                    self.circuit_breaker.record_failure("server_error", latency_ms)
                    await asyncio.sleep(2 ** attempt)
                    continue
                
                if response.status_code != 200:
                    error_data = response.json()
                    self.circuit_breaker.record_failure(f"http_{response.status_code}", latency_ms)
                    return {
                        "success": False,
                        "error": error_data.get("error", {}).get("message", "Unknown error"),
                        "status_code": response.status_code,
                        "circuit_stats": self.circuit_breaker.get_stats()
                    }
                
                data = response.json()
                content = data["choices"][0]["message"]["content"]
                
                parsed = self._extract_json_safely(content)
                
                if parsed is None:
                    self.circuit_breaker.record_failure("json_parse_error", latency_ms)
                    if attempt < max_retries - 1:
                        # Add retry context
                        messages.append({
                            "role": "assistant",
                            "content": content
                        })
                        messages.append({
                            "role": "user",
                            "content": "Previous response was not valid JSON. Please return ONLY a valid JSON object without any additional text."
                        })
                        continue
                    return {
                        "success": False,
                        "error": "Failed to parse JSON after all retries",
                        "raw_response": content[:500]
                    }
                
                # Validate required fields
                required = schema.get("required", [])
                missing = [f for f in required if f not in parsed]
                if missing:
                    self.circuit_breaker.record_failure("missing_fields", latency_ms)
                    if attempt < max_retries - 1:
                        messages.append({"role": "assistant", "content": content})
                        messages.append({
                            "role": "user",
                            "content": f"Missing required fields: {', '.join(missing)}. Please include all required fields in your JSON response."
                        })
                        continue
                    return {
                        "success": False,
                        "error": f"Missing required fields: {', '.join(missing)}",
                        "partial_data": parsed
                    }
                
                self.circuit_breaker.record_success()
                
                # Update session context
                self.session_context.append({"role": "user", "content": prompt})
                self.session_context.append({"role": "assistant", "content": content})
                
                return {
                    "success": True,
                    "data": parsed,
                    "usage": data.get("usage", {}),
                    "latency_ms": round(latency_ms, 2),
                    "circuit_stats": self.circuit_breaker.get_stats()
                }
                
            except httpx.TimeoutException as e:
                latency_ms = (time.time() - start_time) * 1000
                self.circuit_breaker.record_failure("timeout", latency_ms)
                if attempt < max_retries - 1:
                    await asyncio.sleep(2 ** attempt)
                    continue
                return {
                    "success": False,
                    "error": f"Request timeout after {latency_ms:.0f}ms"
                }
                
            except httpx.ConnectError as e:
                latency_ms = (time.time() - start_time) * 1000
                self.circuit_breaker.record_failure("connection_error", latency_ms)
                await asyncio.sleep(5)
                return {
                    "success": False,
                    "error": f"Connection failed: {str(e)}"
                }
                
            except Exception as e:
                return {
                    "success": False,
                    "error": f"Unexpected error: {type(e).__name__}: {str(e)}"
                }
        
        return {
            "success": False,
            "error": "Max retries exceeded",
            "circuit_stats": self.circuit_breaker.get_stats()
        }


Production usage example

async def main(): client = JSONModeResilientClient(api_key="YOUR_HOLYSHEEP_API_KEY") product_schema = { "type": "object", "properties": { "name": {"type": "string"}, "price": {"type": "number"}, "currency": {"type": "string"}, "features": {"type": "array", "items": {"type": "string"}}, "availability": {"type": "string", "enum": ["in_stock", "out_of_stock", "preorder"]} }, "required": ["name", "price", "currency"] } result = await client.structured_completion( prompt="Extract product data: MacBook Pro M4 Max, 3499 USD, in stock, features: 16-core CPU, 40-core GPU, 48GB unified memory, 1TB SSD", schema=product_schema, system_message="You are a precise data extraction assistant. Always return valid JSON matching the exact schema provided." ) if result["success"]: print(f"✓ Parsed successfully in {result['latency_ms']}ms") print(json.dumps(result["data"], indent=2)) else: print(f"✗ Failed: {result['error']}") print(f"Circuit breaker state: {result['circuit_stats']}") await client.close() if __name__ == "__main__": asyncio.run(main())

Solution 3 : Validation et repair automatique avec streaming

Pour les applications temps réel comme les dashboards ou les chatbots, le streaming est indispensable. Voici comment maintenir la validité JSON même en mode streaming :

#!/usr/bin/env python3
"""
Streaming JSON Mode handler with real-time validation
Shows partial JSON status, handles connection drops gracefully
"""

import asyncio
import json
import re
import httpx
from typing import AsyncGenerator, Dict, Any, Optional, Callable
from dataclasses import dataclass
from enum import Enum
import time

class StreamState(Enum):
    WAITING = "waiting"
    IN_JSON = "in_json"
    JSON_COMPLETE = "json_complete"
    ERROR = "error"

@dataclass
class StreamingProgress:
    state: StreamState
    partial_json: str
    validation_errors: list
    is_complete: bool
    bytes_received: int
    last_update_ms: float

class StreamingJSONValidator:
    """
    Validates and repairs JSON in real-time during streaming
    """
    
    def __init__(self, required_fields: list, strict_mode: bool = False):
        self.required_fields = required_fields
        self.strict_mode = strict_mode
        self.state = StreamState.WAITING
        self.buffer = ""
        self.validation_errors = []
        self.json_depth = 0
        self.array_depth = 0
    
    def reset(self):
        self.state = StreamState.WAITING
        self.buffer = ""
        self.validation_errors = []
        self.json_depth = 0
        self.array_depth = 0
    
    def _is_valid_partial_json(self, text: str) -> tuple[bool, list]:
        """Check if current buffer could be valid JSON eventually"""
        errors = []
        
        # Count braces and brackets
        open_braces = text.count('{')
        close_braces = text.count('}')
        open_brackets = text.count('[')
        close_brackets = text.count(']')
        
        depth = (open_braces - close_braces) + (open_brackets - close_brackets)
        
        if close_braces > open_braces:
            errors.append("Extra closing brace")
        if close_brackets > open_brackets:
            errors.append("Extra closing bracket")
        
        # Check for trailing commas before close
        trailing_comma = re.search(r',\s*[}\]]', text)
        if trailing_comma:
            errors.append("Trailing comma detected")
        
        # Check for unclosed strings
        quote_count = text.count('"') - text.count('\\"')
        if quote_count % 2 != 0:
            errors.append("Unclosed string")
        
        return len(errors) == 0, errors
    
    def process_chunk(self, chunk: str) -> StreamingProgress:
        """Process incoming chunk and return validation status"""
        self.buffer += chunk
        
        result, errors = self._is_valid_partial_json(self.buffer)
        
        # Update state machine
        if '{' in chunk and self.state == StreamState.WAITING:
            self.state = StreamState.IN_JSON
        
        # Check for completion
        open_count = self.buffer.count('{')
        close_count = self.buffer.count('}')
        if open_count > 0 and open_count == close_count:
            self.state = StreamState.JSON_COMPLETE
        
        # Try to parse current buffer
        parsed = None
        is_valid = False
        if self.state == StreamState.JSON_COMPLETE:
            try:
                parsed = json.loads(self.buffer)
                
                # Check required fields
                missing = [f for f in self.required_fields if f not in parsed]
                if missing:
                    errors.append(f"Missing fields: {missing}")
                else:
                    is_valid = True
                    
            except json.JSONDecodeError as e:
                errors.append(f"Parse error: {str(e)}")
        
        return StreamingProgress(
            state=self.state,
            partial_json=self.buffer if len(self.buffer) <= 200 else self.buffer[:200] + "...",
            validation_errors=errors,
            is_complete=is_valid and parsed is not None,
            bytes_received=len(self.buffer),
            last_update_ms=time.time() * 1000
        )
    
    def get_final_result(self) -> tuple[bool, Optional[Dict], list]:
        """Return final parsed result after stream completes"""
        try:
            parsed = json.loads(self.buffer)
            missing = [f for f in self.required_fields if f not in parsed]
            
            if missing:
                return False, None, [f"Missing required fields