Saltar a contenido

ADR-012: JSON Schema para forms

  • Status: Accepted
  • Date: 2026-05-14
  • Tags: webapps, tasklist, forms

Context and Problem Statement

User tasks requieren forms para que humanos ingresen data. Camunda soporta 3 tipos: embedded en BPMN XML, deployed forms con formId+version, external forms via formKey. ¿Qué soporta el MVP?

Decision Drivers

  • Forms son standard pattern - reusar tools existentes
  • JSON Schema es industry standard (W3C/IETF)
  • react-jsonschema-form maduro para rendering
  • AJV maduro para validation
  • Embedded forms en BPMN XML coupling tight

Considered Options

  1. JSON Schema forms únicamente (deployed resources)
  2. Soportar los 3 tipos como Camunda (embedded, deployed, external)
  3. External forms únicamente (formKey link)
  4. HTML templates custom

Decision Outcome

Chosen option: JSON Schema forms únicamente porque: - Standard maduro con ecosystem rico - react-jsonschema-form, Vue/Angular equivalents - AJV para server-side validation - Decoupled del BPMN XML (cleaner) - Versionable como recurso independiente

Positive Consequences

  • Tools existing (rjsf, AJV)
  • Validation client + server desde mismo schema
  • Forms versionable independientemente
  • Decoupling BPMN ↔ Forms
  • Cero código de form designer
  • Standard sobrevivirá

Negative Consequences

  • Visual form designer requires external tool
  • Migration desde Camunda forms requires conversion
  • Custom widgets para casos complejos
  • Aprendizaje JSON Schema syntax

Schema storage

CREATE TABLE forms (
    form_key BIGINT PRIMARY KEY,
    form_id TEXT NOT NULL,
    version INT NOT NULL,
    tenant_id TEXT NOT NULL,
    schema JSONB NOT NULL,        -- JSON Schema
    ui_schema JSONB,              -- UI hints
    deployment_key BIGINT NOT NULL,
    deployed_at TIMESTAMPTZ NOT NULL,
    UNIQUE (form_id, version, tenant_id)
);

Ejemplo

{
  "form_id": "loan-application-v1",
  "schema": {
    "type": "object",
    "properties": {
      "amount": { "type": "number", "minimum": 1000, "maximum": 1000000 },
      "purpose": { "type": "string", "enum": ["personal", "business"] },
      "notes": { "type": "string", "maxLength": 500 }
    },
    "required": ["amount", "purpose"]
  },
  "ui_schema": {
    "notes": { "ui:widget": "textarea" }
  }
}

Rendering

import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';

<Form
  schema={form.schema}
  uiSchema={form.ui_schema}
  validator={validator}
  formData={variables}
  onSubmit={({ formData }) => completeTask(formData)}
/>

Server-side validation

import Ajv from 'ajv';
const ajv = new Ajv();

async function completeTask(taskKey, variables) {
  const task = await getTask(taskKey);
  const form = await getForm(task.form_id, task.form_version);

  const validate = ajv.compile(form.schema);
  if (!validate(variables)) {
    throw new ValidationError(validate.errors);
  }

  await persistComplete(taskKey, variables);
}

Doble validación (defense in depth).

Migration path desde Camunda forms

Tool/script de conversion:

mvp-cli forms convert --from=camunda --input=camunda-form.json
# Output: mvp-form.json con JSON Schema

Camunda forms (custom JSON schema) → MVP forms (standard JSON Schema). Conversion automatic para forms simples; manual para complejos.

BPMN reference

En BPMN XML, user task referencia form:

<bpmn:userTask id="review">
  <bpmn:extensionElements>
    <zeebe:formDefinition formId="loan-application-v1" />
  </bpmn:extensionElements>
</bpmn:userTask>

Engine resuelve formId+version a form schema.