Valid JSON syntax is only the first check
JSON.parse tells you whether text can be decoded as JSON. It does not establish that the object contains the fields, types and ranges your application accepts. This recipe applies a small explicit contract before transforming records, then constructs a separate output allowlist. It uses Node 24 with no npm packages.
Scroll horizontally to see every column.
| Input | Rule |
|---|---|
| Top-level object | Only records; an array with 1–50 entries; text at most 16 KiB |
| Each record | Exactly id, label, quantity, unit_cents and enabled |
| id | Unique item- followed by exactly three ASCII digits |
| label | String of at most 120 code points before normalization; 1–64 afterward |
| quantity / unit_cents | Safe integers, respectively 1–1000 and 0–1000000 |
| enabled | A boolean; strings such as true are rejected |
These checks are application code, not a JSON Schema implementation. The current cpuOS template does not provide an npm installation step for a schema library. If you use JSON Schema in your application, validate there as well and agree the same business rules. The ECMAScript JSON.parse specification defines parsing, rather than this record contract.
Normalize text and keep money as integer cents
{ "records": [ { "id": "item-001", "label": " Café ", "quantity": 2, "unit_cents": 350, "enabled": true }, { "id": "item-002", "label": "Résumé ", "quantity": 3, "unit_cents": 1250, "enabled": false }, { "id": "item-003", "label": "GPU Cable", "quantity": 1, "unit_cents": 2400, "enabled": true } ]}The first label contains an e followed by a combining acute accent. NFC normalization yields Café; trimming and collapsing whitespace removes its surrounding spaces. The second record is disabled and is excluded only after its fields are validated. The active records contribute 700 and 2,400 cents, for 3,100 total.
Labels containing remaining control characters or unpaired surrogates are rejected. Unicode normalization does not establish that two human names are the same identity or remove visual confusables. Use stable IDs for matching. String.normalize specification.
Run the complete Node program locally
// Node 24, no packages. Input is data, never executable source.const INPUT_JSON = "{\n \"records\": [\n {\n \"id\": \"item-001\",\n \"label\": \" Café \",\n \"quantity\": 2,\n \"unit_cents\": 350,\n \"enabled\": true\n },\n {\n \"id\": \"item-002\",\n \"label\": \"Résumé \",\n \"quantity\": 3,\n \"unit_cents\": 1250,\n \"enabled\": false\n },\n {\n \"id\": \"item-003\",\n \"label\": \"GPU Cable\",\n \"quantity\": 1,\n \"unit_cents\": 2400,\n \"enabled\": true\n }\n ]\n}"const FIELDS = ["id", "label", "quantity", "unit_cents", "enabled"]function exactObject(value, fields, message) { if (value === null || typeof value !== "object" || Array.isArray(value)) { throw new Error(message) } const keys = Object.keys(value) if (keys.length !== fields.length || fields.some((field) => !Object.hasOwn(value, field))) { throw new Error(message) }}function transform(text) { if (typeof text !== "string" || Buffer.byteLength(text, "utf8") > 16 * 1024) { throw new Error("Input must be JSON text within the recipe's 16 KiB limit.") } let input try { input = JSON.parse(text) } catch { // Do not print the parser's message, which can contain input fragments. throw new Error("JSON syntax invalid.") } exactObject(input, ["records"], "Expected an object containing only records.") if (!Array.isArray(input.records) || input.records.length < 1 || input.records.length > 50) { throw new Error("Expected 1 to 50 records.") } const seen = new Set() const items = [] for (const [index, record] of input.records.entries()) { const prefix = "Record " + (index + 1) + ": " exactObject(record, FIELDS, prefix + "fields do not match the contract.") if (typeof record.id !== "string" || record.id.length !== 8 || !/^item-[0-9]{3}$/.test(record.id) || seen.has(record.id)) { throw new Error(prefix + "invalid or duplicate ID.") } if (typeof record.label !== "string" || [...record.label].length > 120) { throw new Error(prefix + "invalid label type or input length.") } const label = record.label.normalize("NFC").trim().replace(/\s+/gu, " ") if (!label || [...label].length > 64 || /[\p{Cc}\p{Cs}]/u.test(label)) { throw new Error(prefix + "invalid normalized label.") } if (!Number.isSafeInteger(record.quantity) || record.quantity < 1 || record.quantity > 1000) { throw new Error(prefix + "quantity must be an integer from 1 to 1000.") } if (!Number.isSafeInteger(record.unit_cents) || record.unit_cents < 0 || record.unit_cents > 1_000_000) { throw new Error(prefix + "unit_cents must be an integer from 0 to 1000000.") } if (typeof record.enabled !== "boolean") { throw new Error(prefix + "enabled must be a boolean.") } seen.add(record.id) if (record.enabled) { // Construct the output allowlist; never spread the input object. items.push({ id: record.id, label, quantity: record.quantity, line_total_cents: record.quantity * record.unit_cents }) } } return { input_count: input.records.length, active_count: items.length, total_cents: items.reduce((sum, item) => sum + item.line_total_cents, 0), items }}try { console.log(JSON.stringify(transform(INPUT_JSON)))} catch (error) { console.error(error instanceof Error ? error.message : "Input rejected.") process.exitCode = 1}node --versionnode transform-json.mjsInput size is measured in UTF-8 bytes with Node 24 Buffer.byteLength. The parser's original error message is replaced with a fixed diagnostic so malformed input fragments are not written to stderr. Business errors identify a record number and rule, without printing its values.
On failure, the program prints no partial result and sets process.exitCode to 1. On success, it prints one JSON object. It never evaluates data as JavaScript or copies unknown input fields into the output.
Compare the output with its explicit contract
{ "input_count": 3, "active_count": 2, "total_cents": 3100, "items": [ { "id": "item-001", "label": "Café", "quantity": 2, "line_total_cents": 700 }, { "id": "item-003", "label": "GPU Cable", "quantity": 1, "line_total_cents": 2400 } ]}Output items retain their input order and contain only ID, normalized label, quantity and calculated line total. The result records both input and active counts. Requiring every input record to pass validation prevents an invalid disabled row from being silently ignored.
The chosen bounds keep each product and the maximum 50-record sum within JavaScript's safe-integer range. Prices remain integer cents throughout. A different currency precision, larger range or tax calculation needs its own contract and numeric representation.
Save this verifier as verify-result.mjs for the synthetic fixture. It requires the entire parsed result.json object to match the expected transformation. When adapting the input, replace fixture equality with your own versioned result contract and business invariants.
import assert from "node:assert/strict"import { readFileSync } from "node:fs"const expected = { "input_count": 3, "active_count": 2, "total_cents": 3100, "items": [ { "id": "item-001", "label": "Café", "quantity": 2, "line_total_cents": 700 }, { "id": "item-003", "label": "GPU Cable", "quantity": 1, "line_total_cents": 2400 } ]}try { const actual = JSON.parse(readFileSync("result.json", "utf8")) assert.deepStrictEqual(actual, expected) console.log("Fixture result matches.")} catch { // Assertion and parsing errors can contain data; keep diagnostics fixed. console.error("Fixture result did not match the expected contract.") process.exitCode = 1}Run the same source through the Node jobs API
Use the quickstart to connect a Docker worker and create a workspace key. Copy the published client from JavaScript code execution into run-job.mjs, then keep it beside transform-json.mjs on your submitting machine. The client sends that file's source text with template: "node"; it does not install packages or upload an input file.
# CPUOS_API_KEY is already set in your trusted client environment.# Retain this key if you need to recover an uncertain submission.export CPUOS_IDEMPOTENCY_KEY="$(node -p 'crypto.randomUUID()')"node run-job.mjs node transform-json.mjs > result.json && node verify-result.mjsThe client requests 1 CPU, 256 MiB and a 30-second timeout. It accepts a result only after completed status, exit code 0, no execution error and untruncated output, then parses stdout as JSON. Compare that object to the expected result before putting it into a downstream workflow. Reuse the same idempotency key and unchanged source when recovering an ambiguous POST; a new key requests another execution.
The complete submitted source must fit the separate 64 KiB code limit. cpuOS runs trusted team code on your restricted Docker worker, with no job networking, package installation, uploaded files or persistent sessions. Code and output pass through the EU-hosted control plane; your worker can be located elsewhere. Keep API credentials outside the submitted source.
Test contract changes before accepting more inputs
- Test malformed JSON separately from a syntactically valid object with missing, unknown or mistyped fields.
- Test zero, negative, fractional and out-of-range numbers. Do not coerce numeric strings or boolean quantities.
- Test decomposed Unicode, whitespace-only labels, duplicate IDs and invalid disabled records.
- Version the input and output contracts when another application starts consuming this result. Do not quietly add fields or change normalization rules.
Use the result as a checked step in n8n or a controlled LangChain tool. gpuOS open models can propose structured inputs, but valid JSON from a model still requires your contract checks and authorization. For tabular data, use the Python CSV recipe; for lifecycle handling, follow the Python jobs API guide.