Cierre de R7 — Toda la API valida entrada con Ninja Schemas (v1.45.2)
Resumen
Segunda y última mitad de la rectificación de validación de entrada en la auditoría integral s187: los 12 endpoints que quedaban sin tipar migraron de json.loads + dict.get() a Ninja Schemas declarativos. Con esto se cierra R7 completo (P2 en v1.45.0 + P1a en v1.45.1 + P1b en v1.45.2) y, con él, toda la cola de s187 (R1-R7).
Impacto de usuario
Ninguno visible — los campos y comportamiento (errores 400) son idénticos. Es endurecimiento interno: cada endpoint ahora valida y convierte datos de entrada en objetos typados antes de procesarlos, en lugar de manipular diccionarios anónimos.
Cambios en R7 lote P1b
Endpoints migrados (12 total)
| App | Endpoint | Schema | Notas |
|---|---|---|---|
| blueprints | POST /{bp_id}/annotations | AnnotationInputSchema | create_annotation: data libre-form (dict Konva o JSON string) |
POST /{bp_id}/delete_connection | ConnectionDeleteIn | Alias from/to → from_id/to_id | |
POST /{bp_id}/duplicate_element | DuplicateElementIn | Copia de rack o anotación | |
POST /prompt/save | PromptSaveIn | Guardar prompt del AI Brain | |
POST /prompt/save-as | PromptSaveAsIn | Guardar con nombre | |
| signage | POST /signage/playlists/preview-svg | PreviewSvgIn | Items libres (SVG composer los valida internamente) |
POST /signage/deploy-content | DeployContentIn | WebDAV + credenciales opcionales | |
POST /signage/publish-setup | PublishSetupIn | Config de SignagePlayer | |
| network | POST /mib-assistant/apply | MibProposalIn | Merge sin sobrescritura de propuesta de OIDs |
POST /mac/oui-lookup | OuiLookupIn | Batch lookup de MAC → fabricante | |
| terminal | POST /fleet/config | FleetConfigIn | Modo de asignación de roles (auto/manual) |
| core/Help | POST /ask + POST /ask-stream | HelpAskIn | Proxy a workspace bib_ask + variante streaming |
Schemas nuevos creados
# blueprints/api/schemas.py
class AnnotationInputSchema(Schema):
type: str | None = None
data: Any = None # free-form Konva dict/string JSON
class ConnectionDeleteIn(Schema):
from_id: int | str = Field("", alias="from") # alias palabra reservada
to_id: int | str = Field("", alias="to")
type: str = "rack_connection"
class DuplicateElementIn(Schema):
type: str = ""
id: int | None = None
# blueprints/api/prompt.py
class PromptSaveIn(Schema):
content: str = ""
class PromptSaveAsIn(Schema):
name: str = ""
content: str = ""
# signage/api/schemas.py
class PreviewSvgIn(Schema):
items: list[dict] = [] # libre-form — forma interna validada por composer
# monitoring/api/signage/deploy.py
class DeployContentIn(Schema):
profile_id: int | None = None
asset_ids: list[int] = []
username: str = ""
password: str = "" # credenciales WebDAV opcionales
class PublishSetupIn(Schema):
profile_id: int | None = None
clear_action: str | None = None
corporate_asset_id: int | None = None
playlist_id: int | None = None
schedule_id: int | None = None
playback: dict | None = None
# network/api/vendor.py
class MibProposalIn(Schema):
monitoring_oids: dict = {}
deep_discovery_oids: dict = {}
class OuiLookupIn(Schema):
macs: list[str] = []
# terminal/api/fleet.py
class FleetConfigIn(Schema):
role_mode: str = "auto"
# core/api_help.py
class HelpAskIn(Schema):
question: str = ""
mode: str | None = None # "help" | "tutor"
history: list[dict] | None = None # multi-turn chat (tutor)
source_type: str | None = None
app: str | None = None
Correcciones importantes
AnnotationInputSchema reescrito:
- Existía sin uso real y con forma equivocada (
data: str). - Corregido a
data: Any— acepta tanto dict Konva serializado como JSON strings. - Opcional: handler
create_annotationconserva sus propios checks 400.
Excepción deliberada: update_annotation
- Se queda SIN schema a propósito.
- Razón: acepta claves arbitrarias que se mergean dentro de
data(payload libre de Konva por diseño). - Un schema Ninja las descartaría silenciosamente → rompe funcionalidad.
- Documentado en el docstring del endpoint.
Cambios mínimos de implementación
Patrón general:
# Antes
try:
body_data = json.loads(request.body) if request.body else {}
except (json.JSONDecodeError, TypeError):
body_data = {}
value = body_data.get("key", default)
# Ahora
def endpoint(request, payload: SchemaClass):
value = payload.key_name # o payload.dict() si se necesita dict completo
Diff típico: 6-10 líneas de boilerplate json/dict removidas por endpoint. La lógica de negocio intacta.
Hitos de R7 completo
| Lote | Versión | Endpoints | Ámbito |
|---|---|---|---|
| P2 | v1.45.0 | 6 | Endpoints generales (CRUD de blueprints, racks, placements) |
| P1a | v1.45.1 | 5 | Mutadores de racks (create, update, delete, etc.) |
| P1b | v1.45.2 (esta) | 12 | Blueprints (annotations, connections, prompts), signage, network, terminal, Help |
| Total | — | 23 endpoints | Auditoría s187 R1-R7 completada |
Cola s187 cerrada
Con R7 cierran todos los lotes de s187:
- R1: Quirúrgica (RLS, multi-tenancy core)
- R2-R6: Validaciones de entrada, patterns, logging
- R7: Última rectificación de schemas
Quedan aparte: decisiones de stack “M” (psycopg3, Vite 7) que se resolverán en futuras iniciativas. Task #165 (
save_device_network_configmanagement_config) documentado como out-of-scope deliberado.
Patrones técnicos establecidos
Uso de alias en Pydantic
Campos con nombres reservados de Python o conflictos JSON → Field(..., alias="..."):
class ConnectionDeleteIn(Schema):
from_id: int | str = Field("", alias="from") # JSON: "from"
to_id: int | str = Field("", alias="to") # JSON: "to"
Cliente envía {"from": 123, "to": 456} → Ninja deserializa automáticamente.
Campos libres (Any, dict)
Algunos endpoints aceptan estructuras sin forma fija → se documentan explícitamente:
class AnnotationInputSchema(Schema):
data: Any = None # Komva dict serializado o JSON string — validación interna en handler
class PreviewSvgIn(Schema):
items: list[dict] = [] # Forma interna consumida por svg_composer
Alternativa rechazada: json.loads manual. Razón: Ninja Schemas dan tipado + documentación OpenAPI automática, aunque algunos campos sean libres.
Migración progresiva
No fue “big bang”: R7 se dividió en 3 lotes por ciclo de auditoría, permitiendo review y tests granulares. Cada lote en su PR y versión minor.
Verificación y testing
- Contrato HTTP intacto: mismos status codes (200, 400, 404), mismas claves response.
- Defaults preservados: schemas especifican defaults (ej.
role_mode: str = "auto"). - Errores 400 originales mantienen lógica: handlers siguen validando (
if not question,if profile_id is None, etc.). - OpenAPI actualizado automáticamente: Ninja genera docs del schema en
GET /api/schema.
Véase también
- [[entity—blueprints—service—annotations]]
- [[entity—blueprints—service—prompts]]
- [[entity—signage—service—playlists]]
- [[entity—network—service—vendor-mib]]
- [[entity—terminal—service—fleet]]
- [[entity—core—endpoint—help-ask]]
- [[concept—api—input-validation]]
- [[decision—20260630—schemas-ninja-s187]]