EspañolCatalàEnglish
Apariencia
Hablemos

BLOG / Arquitectura

Schema Engine: por qué compilar metadatos antes de renderizar interfaces

Cómo compilar y validar metadatos antes de renderizar interfaces dinámicas sin desplazar la autoridad del dominio al frontend.

La primera versión de un formulario suele ser sencilla. Se escriben controles, validaciones y una llamada para guardar. Después aparecen variantes: crear y editar no muestran exactamente lo mismo; un producto necesita otra etiqueta; un campo es visible pero no editable bajo cierto contexto; una colección requiere identidad estable; otra aplicación necesita proyectar el mismo modelo con un sistema visual diferente.

La repetición suele resolverse primero con componentes reutilizables y después con configuración local. Ese paso puede ser suficiente durante mucho tiempo.

El problema aparece cuando la configuración deja de ser un detalle de una pantalla y empieza a funcionar como un contrato compartido entre productos, versiones o equipos. En ese punto ya no basta con que un renderer recorra un objeto y "haga lo que pueda".

Schema Engine nace de esa frontera. Es un ecosistema neutral respecto al framework que compila metadatos en definiciones de interfaz validadas y normalizadas, y las ejecuta mediante runtimes controlados.

Su primer vertical implementado son los formularios basados en JSON Schema y en un contrato propio de UI Schema. Angular es el primer adapter de referencia, pero no es el propietario del modelo, la validación, las operaciones ni el estado portable.

El proyecto es Public + Experimental. Esta clasificación importa: existe una línea pública verificable, pero todavía no supone una promesa de estabilidad, soporte o adopción productiva.

El problema no es dibujar un input

Convertir una propiedad string en un <input> es la parte fácil. Las preguntas difíciles aparecen alrededor:

  • ¿Qué dialecto y subconjunto del esquema se acepta?
  • ¿Qué sucede ante una combinación no soportada?
  • ¿Dos renderers interpretan igual la misma configuración?
  • ¿Quién es propietario del valor y del baseline?
  • ¿Cómo se representa una operación sobre una ruta profunda?
  • ¿Qué identidad conserva un elemento de colección al reordenarlo?
  • ¿Dónde vive la validación y cómo se normalizan sus errores?
  • ¿Puede la presentación guardar o autorizar por su cuenta?
  • ¿Qué contrato permite sustituir Angular o el design system?

Si cada renderer responde esas preguntas directamente desde el JSON original, el formato aparentemente portable acaba conteniendo varias implementaciones implícitas.

La compatibilidad depende entonces de convenciones que no se verifican de forma explícita, y un error puede no aparecer hasta que el usuario abre una pantalla concreta.

Schema Engine intenta mover parte de esos fallos hacia una frontera determinista anterior al renderizado.

Compilar no significa generar código

En este contexto, compilar significa validar las entradas soportadas y producir una definición normalizada que el runtime y los renderers puedan consumir sin reinterpretar el esquema original.

El flujo conceptual es:

SchemaEngine: De esquema a interfaz

La distinción entre esquema válido y esquema soportado es importante.

Un JSON Schema puede ser correcto según su dialecto y utilizar al mismo tiempo construcciones que un vertical concreto de Schema Engine no implementa.

Aceptarlo y degradarlo silenciosamente sería peor que rechazarlo: dos consumidores podrían mostrar interfaces distintas creyendo que comparten el mismo contrato.

Por eso la compilación produce diagnósticos explícitos cuando encuentra incompatibilidades.

No garantiza que la configuración defina una buena experiencia de usuario ni que una regla de negocio sea correcta. Reduce un tipo de riesgo más concreto: errores estructurales y combinaciones incompatibles que pueden detectarse antes de entregar la definición al renderer.

Definiciones normalizadas, no metadatos crudos

Una definición normalizada elimina decisiones que no deberían repetirse en cada capa visual.

Contiene rutas canónicas, semántica de campos, nulabilidad, estructura recursiva y contratos de presentación dentro del subconjunto aceptado.

El renderer no recibe libertad para volver a interpretar JSON Schema. Recibe una definición cuyo significado ya ha sido acotado.

Puede decidir cómo proyectar un campo compatible con su design system, pero no inventar una semántica diferente para un tipo, una ruta o una operación.

Esta separación tiene un coste. Hay que versionar contratos, mantener diagnósticos y evitar que excepciones específicas de una aplicación acaben entrando en el core.

Para una interfaz estática conocida durante el desarrollo, ese coste puede ser injustificado. Schema Engine no pretende sustituir todo el código de presentación.

Resulta especialmente útil cuando la definición cambia con independencia del despliegue del frontend, cuando un mismo modelo necesita varias proyecciones, cuando varias aplicaciones requieren un comportamiento consistente o cuando la definición pertenece a otro actor distinto del componente que la renderiza.

El runtime no es un formulario que guarda por su cuenta

El runtime expone snapshots inmutables y recibe operaciones estrictas. La aplicación sigue siendo la fuente de verdad para el valor completo y su baseline.

Cuando una interacción propone modificar una ruta, el motor emite una operación que la aplicación puede aceptar, rechazar o diferir.

Si la acepta, la aplicación devuelve el nuevo estado autoritativo.

El renderer no adquiere por ello propiedad sobre el modelo y el motor tampoco empieza a controlar HTTP, persistencia o procesos de guardado.

Control del estado

Esta frontera impone un flujo de datos unidireccional estricto.

Puede parecer más ceremonioso que un two-way binding directo, y lo es. A cambio, hace visibles decisiones que en interfaces complejas suelen quedar repartidas entre componente, store y servicio:

  • qué intención emitió el usuario;
  • sobre qué ruta e identidad estable actuó;
  • qué validación correspondía al snapshot;
  • qué estado aceptó finalmente la aplicación;
  • qué responsabilidades permanecen fuera del runtime.

No todas las aplicaciones necesitan ese grado de control.

En las que sí lo necesitan, ocultarlo detrás de mutaciones locales no elimina la complejidad. Sólo hace más difícil seguir su rastro.

Validar datos no es autorizar operaciones

Schema Engine admite validadores sustituibles y normaliza sus resultados mediante un contrato propio.

El repositorio incluye una integración privada de referencia con Ajv para Draft 2020-12, pero Ajv no es una dependencia del core público.

Esa validación responde a preguntas relacionadas con los datos y el contrato del formulario.

No decide si una persona puede modificar una factura, publicar un artículo o consultar un campo confidencial.

En una evolución futura, una aplicación podría suministrar permisos efectivos para que una proyección oculte un campo o lo muestre como readonly.

Eso seguiría siendo una decisión de presentación.

El servidor y la capa de aplicación deben continuar autorizando cada operación protegida. Convertir los metadatos de UI en autoridad de seguridad significaría entregar a una entrada manipulable una responsabilidad que no le corresponde.

Neutral respecto al framework no significa sin adapters

El core no depende de Angular, React, Vue, RxJS, DOM ni de objetos globales del navegador.

Eso no significa que la interfaz aparezca por sí sola. Cada plataforma necesita un adapter que conecte sus mecanismos de renderizado y reactividad con los snapshots y operaciones del core.

Un core, varios adapters y renders

El puente del core es deliberadamente pequeño y síncrono.

subscribe() entrega snapshots inmutables mediante callbacks y subscribeOperations() entrega las operaciones solicitadas por el usuario.

Cuando se acepta la suscripción, ambos canales devuelven una función unsubscribe idempotente.

El adapter Angular copia cada snapshot recibido a un Signal interno y proyecta las operaciones mediante un output. No necesita introducir RxJS ni EventTarget en el core.

Otro adapter puede conectar esos mismos callbacks con el mecanismo natural de su plataforma.

La implementación actual incluye un adapter headless para Angular 22, renderers HTML nativos accesibles y un piloto separado para contenedores Angular Aria.

La separación entre adapter, renderer y piloto es deliberada: sustituir un design system no debería redefinir el contrato portable.

La neutralidad tampoco puede demostrarse únicamente eliminando imports de Angular.

El repositorio mantiene dos aplicaciones de referencia privadas: una en Angular y otra sin framework, denominada Standard/DOM dentro del proyecto, que consume directamente el core mediante TypeScript y APIs DOM nativas.

No utiliza Web Components ni constituye un adapter público.

Ambas aplicaciones aportan evidencia de que el contrato actual puede ejecutarse sin Angular. Esto no implica una matriz de compatibilidad con todos los frameworks.

React, Vue y Angular legacy continúan diferidos.

Por qué JSON Schema y un UI Schema propio

JSON Schema ofrece una especificación pública para describir la estructura y las restricciones de los datos.

Schema Engine toma Draft 2020-12 como dialecto de referencia, pero su compilador implementa sólo un subconjunto explícito.

UI Schema, en cambio, no designa aquí un estándar externo.

Schema Engine define su propio contrato estricto y versionado para expresar orden, textos, agrupación y otras intenciones de presentación que no pertenecen al esquema de datos.

Distintas herramientas de generación de formularios utilizan alguna variante de esta separación entre datos y presentación, pero no existe un contrato de UI Schema compartido por todas ellas.

Que dos motores utilicen el mismo nombre no implica que acepten la misma estructura, interpreten igual sus miembros o puedan intercambiar definiciones.

En Schema Engine, la única fuente normativa son sus contratos públicos y sus especificaciones versionadas.

Separar ambos conceptos evita utilizar títulos, descripciones u otras anotaciones del dominio como un lenguaje visual accidental. También permite que un mismo modelo tenga varias proyecciones.

Pero ninguno de los dos debe presentarse como un lenguaje universal para construir cualquier interfaz.

Tablas, formularios y property inspectors comparten rutas, campos, diagnósticos y algunas reglas de presentación. Sin embargo, sus runtimes son diferentes:

  • un formulario gestiona interacción, dirty state y visibilidad de errores;
  • una tabla añade filas, columnas, selección, orden, filtrado y paginación;
  • un inspector parte normalmente de una selección contextual y de una edición compacta o inmediata.

Forzar esas familias dentro de una única gramática antes de implementarlas produciría una abstracción especulativa.

La dirección del proyecto es compartir únicamente aquello que dos verticales reales demuestren que tienen en común.

Qué existe hoy

A fecha de corte, el vertical de formularios cubre el objeto raíz, objetos anidados, hojas primitivas y colecciones homogéneas de objetos con identidad estable proporcionada por la aplicación.

También incluye un subconjunto estático de referencias locales a $defs, contenedores de presentación estáticos y contratos de texto localizable.

No están activos, entre otros:

  • referencias externas o dinámicas;
  • arrays de primitivas y tuplas;
  • composición general de esquemas;
  • identidad generada por la librería;
  • validación asíncrona;
  • persistencia y workflows;
  • tablas y property inspectors;
  • expresiones dinámicas y proyección por permisos.

Estado actual

Mantener una lista explícita de exclusiones también forma parte del producto.

Permite que un equipo evalúe su encaje real sin confundir una visión a largo plazo con una capacidad que ya puede utilizarse.

Hacia dónde queremos llevarlo

La dirección futura busca proyectar un mismo modelo según el propósito de la vista, una variante seleccionada por la aplicación y el contexto efectivo que ésta suministra.

Un backoffice podría utilizar una proyección de edición. Un flujo de aprobación podría mostrar una versión readonly con acciones distintas. Otro producto podría utilizar un renderer diferente sobre el mismo contrato normalizado.

También queremos estudiar verticales de tablas y property inspectors.

No se trata simplemente de añadir un type: table al formato actual.

Cada familia necesitará su propio contrato, runtime, modelos de fallo, SPEC y evidencia antes de poder promoverse.

Los usos candidatos incluyen plataformas internas configurables, productos multi-tenant, herramientas administrativas, editores de propiedades y sistemas en los que las definiciones evolucionan con independencia del cliente.

No son casos de adopción demostrados. Son contextos donde esta frontera arquitectónica podría justificar su coste.

Finalidad: reducir repetición sin ocultar autoridad

Un motor dirigido por metadatos puede reducir la implementación repetida.

También puede acabar creando un nuevo monolito declarativo, difícil de entender y depurar, si intenta controlar dominio, presentación, persistencia y seguridad desde un mismo JSON.

Schema Engine toma la dirección contraria: compilar un subconjunto explícito, normalizar su significado, ejecutar operaciones controladas y mantener la autoridad en la aplicación.

El proyecto tendrá éxito si varios renderers y productos pueden compartir un contrato sin perder diagnósticos, accesibilidad ni control.

No si consigue expresar cualquier interfaz imaginable dentro de una gramática cada vez más grande.

Ese es también el motivo para mantener la etiqueta Experimental.

La arquitectura ya puede inspeccionarse. Su estabilidad y utilidad a largo plazo todavía deben demostrarse mediante contratos, documentación, releases y adopción real.

Referencias