Zum Hauptinhalt springen

Typisierte Vorlagendaten

Eine Vorlage liest Felder aus data. Schickt Ihr Code invoice.numbr statt invoice.number, gelingt das Rendering trotzdem und druckt eine leere Zelle. Typisierte Vorlagendaten verlegen diesen Fehler in Ihren Compiler: formfeed types erzeugt aus dem Datenschema einen Typ pro Vorlage, und die SDKs prüfen data dagegen.

await client.renders.create({
template: 'invoice',
data: {
number: 'RE-2026-001',
status: 'sent',
customer: { nam: 'Fennlor' },
items: [],
},
// ~~~ Compilerfehler: InvoiceCustomer hat kein Feld "nam"
});

Typen erzeugen​

Führen Sie formfeed types in einem Projekt aus (dem Ordner mit formfeed.json):

npx formfeed types # TypeScript: formfeed.d.ts im Projektordner
npx formfeed types --lang python # Python: formfeed_templates.py
npx formfeed types invoice delivery-note --out src/formfeed.d.ts

Ohne Slugs werden alle Vorlagen des Projekts aufgenommen. Die erzeugte Datei braucht keinen eigenen Laufzeitcode: Die TypeScript-Datei deklariert nur Typen, das Python-Modul importiert nur typing, typing_extensions (wird mit dem Paket formfeed installiert) und formfeed. Committen Sie die Datei und führen Sie den Befehl erneut aus, sobald sich die Daten einer Vorlage ändern.

Legen Sie die Standardwerte einmal in formfeed.json fest, damit alle Entwickler und die CI dieselbe Datei erzeugen:

{
"workspace": "fennlor/production",
"types": { "lang": "ts", "out": "src/formfeed.d.ts" }
}
FeldBedeutung
types.langts (Standard) oder python
types.outAusgabedatei relativ zum Projektordner; ohne Angabe formfeed.d.ts oder formfeed_templates.py
types.sdkModuleDas Modul, das die TypeScript-Datei erweitert; standardmäßig @formfeed/sdk. Setzen Sie es, wenn Sie das SDK unter einem anderen Namen importieren.

Dieselben Optionen gibt es als Flags (--lang, --out, --sdk-module); ein Flag hat Vorrang vor formfeed.json.

Woher das Schema kommt​

Für jede Vorlage nimmt formfeed types die erste Quelle, die vorhanden ist:

ModusQuelle
In einem Projekttemplates/<slug>/schema.json; sonst ein Schema, das über alle Datensätze in templates/<slug>/data/*.json abgeleitet wird
Außerhalb eines Projekts oder mit --remote oder --channel <name>Die API: das gespeicherte data_schema der Version, die der Channel rendert (standardmäßig published); sonst ein Schema, das aus den Beispieldaten und Datensätzen dieser Version abgeleitet wird

Ist die API die Quelle und nennen Sie keine Slugs, werden alle Vorlagen des Workspace aufgenommen; eine nie veröffentlichte Vorlage wird durch ihren neuesten Entwurf beschrieben, und mit --channel wird eine Vorlage ohne diesen Channel übersprungen. Ein Slug, den Sie nennen, muss eine Version auf dem Channel haben.

Der Kopf der erzeugten Datei nennt pro Vorlage die Herkunft ihrer Typen:

// invoice: stored schema of version 4 (channel published)
// delivery-note: inferred from sample data, every field optional: store a schema to make this a contract

Abgeleitete Schemas machen jedes Feld optional. Beispieldaten zeigen, welche Felder es gibt, nicht, auf welche die Vorlage nicht verzichten kann. Der erzeugte Typ akzeptiert deshalb Daten, denen jedes dieser Felder fehlen darf. Falsch geschriebene Felder und falsche Typen fängt er trotzdem ab. Für Pflichtfelder speichern Sie ein Schema.

Abweichungen in der CI finden​

--check schreibt nichts. Es vergleicht die Datei auf der Platte mit dem, was erzeugt würde, und endet mit Exit-Code 1, wenn beides abweicht. Ein Pull Request, der ein Schema oder einen Datensatz ändert, ohne die Typen neu zu erzeugen, schlägt so fehl:

- run: npx formfeed types --check --ci

Zeilenenden zählen nicht als Unterschied, ein Checkout unter Windows besteht also. Mit --json listet das Ergebnis jede Vorlage mit Quelle und Version.

Typen in TypeScript verwenden​

Die erzeugte formfeed.d.ts erweitert das Interface FormfeedTemplates des SDK:

// formfeed.d.ts (erzeugt)
declare module '@formfeed/sdk' {
interface FormfeedTemplates {
'delivery-note': DeliveryNoteData;
invoice: InvoiceData;
}
}

export interface InvoiceData {
/** Invoice number as printed */
number: string;
status: 'draft' | 'sent' | 'paid';
customer: InvoiceCustomer;
items: InvoiceItem[];
tags?: string[];
}

Die Datei muss Teil Ihres TypeScript-Programms sein (innerhalb von include in Ihrer tsconfig.json). Danach prüfen renders.create und renders.batch die data jeder Vorlage, die die Datei kennt:

import { Formfeed, type TemplateData } from '@formfeed/sdk';

const client = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });

// data ist InvoiceData: ein fehlendes Pflichtfeld oder ein falscher Typ ist ein Compilerfehler
await client.renders.create({
template: 'invoice',
data: {
number: 'RE-2026-001',
status: 'sent',
customer: { name: 'Fennlor' },
items: [],
},
});

// die data jedes Eintrags wird gegen die Vorlage des Batches geprüft
await client.renders.batch({
template: 'invoice',
items: invoices.map((data) => ({ data })),
});

// der Datentyp einer Vorlage, für eigene Funktionen
function invoiceData(order: Order): TemplateData<'invoice'> {
/* … */
}

Untypisiert bleibt, damit ein SDK-Update oder neu erzeugte Typen nie einen Build brechen:

  • ein Slug, den die erzeugte Datei nicht kennt, und eine Vorlage, die über ihre tpl_…-ID übergeben wird;
  • ein Vorlagenname in einer string-Variablen statt eines Literals;
  • Renderings aus html und url.

Diese behalten data?: Record<string, unknown>. Für eigene Wrapper exportiert das SDK außerdem FormfeedTemplates, TemplateData<T> und TypedRenderRequest<T>.

Typen in Python verwenden​

Das erzeugte Modul enthält ein TypedDict pro Vorlage und einen Wrapper mit einer Methode pro Vorlage, benannt nach dem Slug (delivery-note wird delivery_note):

from formfeed import Formfeed
from formfeed_templates import InvoiceData, typed

client = Formfeed(api_key)
renders = typed(client)

data: InvoiceData = {"number": "RE-2026-001", "status": "sent", "customer": {"name": "Fennlor"}, "items": []}
render = renders.invoice(data, filename="invoice.pdf", mode="async")

Jede Methode nimmt die Daten der Vorlage als erstes Argument und reicht alle weiteren Keyword-Argumente an client.renders.create weiter; sie liefert also dasselbe Render. Für AsyncFormfeed verwenden Sie typed_async(client) und await auf die Methoden. Optionale Felder sind NotRequired[...]; mypy und Pyright melden einen fehlenden Pflichtschlüssel, einen unbekannten Schlüssel oder einen falschen Typ. Geprüft werden die Typen nur von Ihrem Type-Checker; zur Laufzeit sind die Daten ein gewöhnliches dict. Um Daten auf dem Server zu prüfen, senden Sie validate_data=True mit dem Rendering; siehe Daten auf dem Server prüfen.

Ein Schema zum Vertrag machen​

Ein gespeichertes Datenschema ist ein Versprechen über die Daten, die eine Vorlage annimmt. Es macht die erzeugten Felder dort zu Pflichtfeldern, wo das Schema es sagt, validate_data prüft dagegen, und die Schema-Prüfung vergleicht es, bevor eine Version live geht: Nur gespeicherte Schemas können eine brechende Änderung aufhalten. Das Veröffentlichen einer Version (oder das Verschieben eines Channels auf sie), deren gespeichertes Schema Aufrufer der ersetzten Version bricht, wird mit 409 schema_breaking_change abgelehnt, solange Sie es nicht erlauben (allow_breaking, siehe Trotzdem fortfahren); hat eine der beiden Seiten nur ein abgeleitetes Schema, meldet die Prüfung die Änderung nur.

Öffnen Sie im Editor das Panel Daten und Schema. Der Schema-Bereich listet jedes Feld mit seinem Typ, ob die Vorlage es verwendet, und welche Felder die Vorlage liest, die in den Daten fehlen.

  1. Wählen Sie Als Contract hinterlegen. Der Editor leitet ein Schema über alle Datensätze ab: Ein Feld, das jeder Datensatz enthält, ist zunächst Pflicht, eines, das nur einige enthalten, optional.
  2. Gehen Sie die Spalte Pflicht durch: Schalten Sie sie für jedes Feld ein, auf das die Vorlage nicht verzichten kann, und für jedes aus, auf das sie verzichten kann.
  3. Speichern Sie. Das Schema wird mit der nächsten Version gespeichert.

Aus Daten aktualisieren leitet den Contract neu ab, nachdem Sie die Datensätze geändert haben (Ihre Einstellungen unter Pflicht beginnen dann von vorn), und Contract entfernen löscht ihn mit der nächsten Version. Der Editor bearbeitet Typen und required; für alles Weitere (Formate, Muster, Enums) schreiben Sie schema.json mit der CLI oder senden data_schema mit einer Version über die API (POST /templates/{id}/versions).

Was der Generator versteht​

Objekte mit properties und required; Arrays mit items; string, number, integer, boolean und null; enum und const; anyOf und oneOf als Unions, allOf als Schnittmenge; type-Arrays; lokale $ref in $defs (oder definitions). additionalProperties als Schema ergänzt eine Index-Signatur ([key: string]: unknown in TypeScript); true oder fehlendes additionalProperties ergänzt nichts, ein falsch geschriebener Schlüssel in einem Objektliteral bleibt also ein Fehler. title, description, examples und format werden zu Doc-Kommentaren. Schlüsselwörter, die ein Typ nicht ausdrücken kann (not, if/then/else, patternProperties, prefixItems und ähnliche), werden zu unknown oder Any mit einem Kommentar, der sie nennt. Verschachtelte Objekte erhalten Namen aus Slug und Property-Pfad (InvoiceCustomer, InvoiceItem für die Elemente von items).

Daten auf dem Server prüfen​

Typen fangen Fehler in Code, den Sie kompilieren. Damit Formfeed die Daten eines Renderings selbst gegen das gespeicherte Schema der Version prüft, senden Sie validate_data: true:

curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "Content-Type: application/json" \
-d '{"template": "invoice", "validate_data": true, "data": {"number": 42, "customer": {}}}'

Die Prüfung läuft, bevor etwas gerendert oder gezählt wird. Daten, die nicht passen, beantwortet die API mit 422 data_validation_error, das jede Abweichung in errors auflistet:

{
"type": "https://docs.formfeed.dev/errors/data-validation",
"title": "Data validation failed",
"status": 422,
"code": "data_validation_error",
"detail": "data.items: is required (and 2 more)",
"errors": [
{ "path": "data.items", "message": "data.items: is required" },
{ "path": "data.number", "message": "data.number: must be string" },
{
"path": "data.customer.name",
"message": "data.customer.name: is required"
}
]
}
  • Ohne validate_data wird nichts geprüft, egal was das Schema sagt.
  • Es braucht ein gespeichertes Schema: Eine Version ohne beantwortet die API mit 400 invalid_request, ebenso ein Rendering von html oder einer url.
  • format (email, date) wird nicht geprüft; type, required, enum, pattern und die übrigen Schlüsselwörter schon.
  • In einem Batch wird jeder Eintrag geprüft, der validate_data sendet, und es wird nichts eingereiht, solange nicht alle bestehen.
  • In den SDKs heißt es validate_data: true (TypeScript) und validate_data=True (Python). Um Daten ohne Rendering zu prüfen, nutzen Sie POST /templates/{id}/validate.

Siehe auch​