cpuos

Tutorials · 7 min read · updated Oct 7, 2026

Validate and transform JSON with Node.js

Check JSON syntax, fields, types and bounds with Node 24. Normalize Unicode labels and return an explicit output contract as a cpuOS job.

The current pilot runs trusted Python and Node jobs on your Docker worker. Containers share its kernel. Browser and repository workflows need capabilities beyond this pilot.

On this page

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.

InputRule
Top-level objectOnly records; an array with 1–50 entries; text at most 16 KiB
Each recordExactly id, label, quantity, unit_cents and enabled
idUnique item- followed by exactly three ASCII digits
labelString of at most 120 code points before normalization; 1–64 afterward
quantity / unit_centsSafe integers, respectively 1–1000 and 0–1000000
enabledA 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

Synthetic JSON embedded as text in the program
{  "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

Save as transform-json.mjs; Node 24 without packages
// 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}
Execute the fixture on your development machine
node --versionnode transform-json.mjs

Input 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

Expected transformed result
{  "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.

verify-result.mjs: check this JSON fixture's result
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.

Submit a Node job with the shared client
# 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.mjs

The 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.

Questions

Does JSON.parse validate my application's schema?
No. It checks JSON syntax and produces JavaScript values. This recipe separately checks allowed fields, record counts, types, numeric bounds, IDs and normalized labels.
Is this a JSON Schema validator?
No. It is a small hand-written contract using Node's built-in language features. A JSON Schema library in your application may complement those checks; this cpuOS recipe does not install npm dependencies.
Why validate a disabled record before excluding it?
The complete batch must satisfy its contract. Otherwise, invalid data could be hidden by a filter and later become active. This recipe rejects the entire input before printing a transformed result.
Does NFC normalization prove two labels refer to the same item?
No. NFC standardizes canonically equivalent Unicode sequences. It does not remove visual confusables or establish identity. Use the unique stable item ID for matching and the normalized label for display.

Related

Connect a worker and run a job

Start with a small trusted Python or Node task, fixed limits and an expected result.

gpuOS · where models think

Need the model too? Run it on gpuOS

gpuOS serves open models on your own GPUs behind one OpenAI-compatible API. Your application can submit authorized actions to cpuOS jobs.