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" }
}
| Feld | Bedeutung |
|---|---|
types.lang | ts (Standard) oder python |
types.out | Ausgabedatei relativ zum Projektordner; ohne Angabe formfeed.d.ts oder formfeed_templates.py |
types.sdkModule | Das 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:
| Modus | Quelle |
|---|---|
| In einem Projekt | templates/<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
htmlundurl.
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.
- Im Editor
- Mit der CLI
Ö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.
- 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.
- 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.
- 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).
Legen Sie ein JSON Schema unter templates/<slug>/schema.json ab und pushen Sie:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["number", "customer", "items"],
"properties": {
"number": { "type": "string", "description": "Invoice number as printed" },
"status": { "enum": ["draft", "sent", "paid"] },
"customer": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"email": { "type": ["string", "null"], "format": "email" }
}
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": { "qty": { "type": "integer" } }
}
}
}
}
npx formfeed templates push invoice
npx formfeed types
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_datawird 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 vonhtmloder einerurl. format(email,date) wird nicht geprüft;type,required,enum,patternund die übrigen Schlüsselwörter schon.- In einem Batch wird jeder Eintrag geprüft, der
validate_datasendet, und es wird nichts eingereiht, solange nicht alle bestehen. - In den SDKs heißt es
validate_data: true(TypeScript) undvalidate_data=True(Python). Um Daten ohne Rendering zu prüfen, nutzen SiePOST /templates/{id}/validate.
Siehe auch
- Release Channels: welche Version ein Rendering verwendet, und die Schema-Prüfung, die das Veröffentlichen absichert.
- Befehlsreferenz: alle Flags von
formfeed types. - TypeScript-SDK und Python-SDK.