OSS-first docs
These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.
Build
Import flexible files with data-exchange templates
Publish one recommended import shape, then let users import CSV/JSON/XML files with partner-specific headers, skipped rows, localized values, and alternate metadata layouts.
What you'll build
A canonical template with target fields, aliases, and formats.
A dry-run import that reports confidence, gaps, and ignored columns.
A client review state where users remap, update formats, or accept inferred mappings.
1) Define the template
Use
defineDataExchangeTemplate
for neutral import/export naming.defineImportTemplate
anddefineExportTemplate
remain available aliases.import {
createImportPlan,
createRecordBatch,
defineDataExchangeTemplate,
previewImport,
} from "@contractspec/lib.data-exchange-core";
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";
const AccountImportSchema = defineSchemaModel({
name: "AccountImport",
fields: {
id: { type: ScalarTypeEnum.ID(), isOptional: false },
status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
amount: { type: ScalarTypeEnum.Float_unsecure(), isOptional: false },
active: { type: ScalarTypeEnum.Boolean(), isOptional: false },
tags: { type: ScalarTypeEnum.JSON(), isOptional: true },
},
});
const accountImportTemplate = defineDataExchangeTemplate({
key: "accounts.import",
version: "1.0.0",
title: "Account import",
columns: [
{
key: "id",
label: "Account ID",
targetField: "id",
required: true,
sourceAliases: ["Account Identifier", "External ID", "No compte"],
},
{
key: "status",
label: "Status",
targetField: "status",
required: true,
sourceAliases: ["Statut", "State"],
format: { kind: "text", trim: true, case: "lowercase" },
},
{
key: "amount",
label: "Amount",
targetField: "amount",
required: true,
sourceAliases: ["Montant", "Balance"],
format: { kind: "number", decimalSeparator: ",", thousandsSeparator: "." },
},
{
key: "active",
label: "Active",
targetField: "active",
sourceAliases: ["Actif", "Enabled"],
format: { kind: "boolean", trueValues: ["yes", "oui"], falseValues: ["no", "non"] },
},
{
key: "tags",
label: "Tags",
targetField: "tags",
format: { kind: "split", delimiter: ";" },
},
],
});
const sourceBatch = createRecordBatch([
{
"No compte": "acc-1",
Statut: " Active ",
Montant: "1.234,50",
Actif: "oui",
Tags: "vip; beta",
},
]);
const preview = previewImport(
createImportPlan({
source: { kind: "memory", batch: sourceBatch, format: "csv" },
target: { kind: "memory", format: "json" },
schema: AccountImportSchema,
sourceBatch,
template: accountImportTemplate,
}),
);
console.log(preview.plan.mappingSource); // "template"
console.log(preview.normalizedRecords[0]);Mapping precedence
Explicit mappings win first, so existing integrations can keep their current mapping arrays.
Template resolution checks exact headers, aliases, normalized labels, and SchemaModel fallback inference.
Format profiles can override formats by target field or template column key without changing the template.
Required template columns become visible preview issues when no source column can be matched.
Supported value formats
text trim and case normalization
localized numbers with decimal and thousands separators
custom true/false boolean labels
dates and datetimes with accepted input formats
JSON parsing, empty-as-null, and default values
split/join delimiters, currency symbols, and percentages
2) Dry-run partner CSV, JSON, or XML files on the server
File, HTTP, and storage adapters accept codec options. CSV can set delimiters, quotes, skipped rows, header rows, or explicit columns. JSON can read records and metadata from custom keys. XML can use custom root, record, metadata, and attribute fields.
import { defineDataExchangeTemplate } from "@contractspec/lib.data-exchange-core";
import { dryRunImport, executeImport } from "@contractspec/lib.data-exchange-server";
const template = defineDataExchangeTemplate({
key: "accounts.import",
version: "1.0.0",
columns: [
{ key: "id", label: "Account ID", targetField: "id", required: true, sourceAliases: ["Account Identifier"] },
{ key: "amount", label: "Amount", targetField: "amount", format: { kind: "currency", currencySymbol: "€", decimalSeparator: "," } },
],
});
const partnerSource = {
kind: "file",
path: "partner-accounts.csv",
format: "csv",
codecOptions: { csv: { delimiter: ";", skipRows: 1 } },
} as const;
const formatProfile = {
columns: {
amount: { kind: "currency", currencySymbol: "€", decimalSeparator: "," },
},
} as const;
const preview = await dryRunImport({
source: partnerSource,
target: { kind: "memory", format: "json" },
schema: AccountImportSchema,
template,
formatProfile,
});
const blockingIssues = preview.issues.filter((issue) => issue.severity === "error");
if (blockingIssues.length === 0) {
await executeImport({
source: partnerSource,
target: { kind: "memory", format: "json" },
schema: AccountImportSchema,
template,
formatProfile,
});
}3) Let users review the mapping
The shared controller exposes template rows, matched source columns, confidence, required status, formatting summaries, unmatched required rows, and ignored source columns. Actions let users remap columns, select aliases, update field formats, reset to the template, or accept inferred mappings.
"use client";
import { useDataExchangeController } from "@contractspec/lib.data-exchange-client";
export function ImportMappingReview({ preview }) {
const controller = useDataExchangeController({ preview });
const replacementSourceColumn = controller.model.ignoredSourceColumns[0];
return (
<section>
{controller.model.templateRows.map((row) => (
<button
key={row.id}
type="button"
disabled={!replacementSourceColumn}
onClick={() => {
if (!replacementSourceColumn) return;
controller.selectAlias(row.targetField, replacementSourceColumn);
}}
>
{row.label}: {row.sourceField || "Unmatched"} -> {row.targetField}
{row.required ? " required" : ""}
{row.formatLabel ? ` (${row.formatLabel})` : ""}
</button>
))}
{controller.model.unmatchedRequiredRows.length > 0 ? (
<p>Resolve required columns before import.</p>
) : null}
</section>
);
}4) Verify the package stack
cd packages/libs/data-exchange-core && bun test && bun run typecheck && bun run lint:check
cd packages/libs/data-exchange-client && bun test && bun run typecheck && bun run lint:check
cd packages/libs/data-exchange-server && bun test && bun run typecheck && bun run lint:checkPrompt: build a template
You are adding an import flow to a ContractSpec app.
Define a reusable data-exchange template for this canonical schema:
- target fields, required flags, and display labels
- known partner column aliases
- value formats for numbers, dates, booleans, JSON, split/join lists, currency, and percentages
Wire the template into core preview planning and server dry-run execution. Keep explicit mappings higher precedence than template resolution. Return the template, preview call, server dry-run call, and tests for alias matching plus localized formatting.Prompt: inspect a partner file
A partner sent a CSV/JSON/XML file that does not match our recommended import template.
Compare the incoming headers and value samples against this ContractSpec data-exchange template. Propose:
- source-to-target column matches with confidence
- missing required target fields
- ignored source columns
- format overrides for localized numbers, booleans, dates, JSON, split/join lists, currency, or percentages
Do not execute the import. Produce a dry-run plan and the user-facing review copy.Build a contract-driven form
Define schema-backed form data, fields, layout, policy, and submit actions as one reusable ContractSpec surface.
Generate docs, clients, and schemas
Export stable docs and client-facing artifacts from the same contract layer.
Why ContractSpec
Keep educational and comparison content reachable without letting it define the primary OSS learning path.