Skip to content

Catching mistakes before they run

In this tutorial you’ll break a mapping twice, let the validator find both problems without running anything, and finish with an evaluation that only happens once the mapping is known good.

It builds on Extending the mapper: you’ll reuse the project and the slugify transformer from there. If you skipped it, any project with mapper-js installed and a slugify transformer registered will do.

Save this as post.yaml. Both mistakes are the realistic kind: a wrong operand and a typo:

mapping:
/post/title: /title
/post/slug: { source: /title, transform: sluggify }
/post/words: { source: /words, as: integer }

Note the shape: the pairings sit under a mapping: key. That is the form the validator takes; map accepts bare pairings and wraps them itself; validate does not.

Save this as check.js:

import Mapper, { validate } from '@christiansmith/mapper-js'
import { parse } from '@std/yaml'
const post = parse(await Deno.readTextFile('post.yaml'))
console.log(JSON.stringify(validate(post), null, 2))

Run deno run --allow-read check.js. The report finds the bad operand:

valid: false
errors:
- rule: KW-pipeline-1
message: as must be string, number, boolean, or json
pointer: /mapping/~1post~1words/as
descriptor: integer
warnings: []

Read the diagnostic inside out: descriptor is the offending value, pointer is where it sits in your mapping (~1 is how a pointer spells / inside a key), and rule names the requirement it violates. Nothing ran; validation never evaluates.

Notice what it did not find: sluggify looks fine to a document-level check. Whether a transformer exists is a fact about an instance, not about the document.

3. Fix the operand, then validate against the instance

Section titled “3. Fix the operand, then validate against the instance”

Change as: integer to as: number in post.yaml. Then ask the mapper you actually run, the one from Extending the mapper with slugify registered, to validate:

const mapper = new Mapper({}, { initializers: {}, transformers, plugins: {} })
console.log(JSON.stringify(mapper.validate(post), null, 2))
valid: false
errors:
- rule: KW-transform-1
message: sluggify matches no registered transformer
pointer: /mapping/~1post~1slug/transform
descriptor: sluggify
warnings: []

The typo is caught, by name, at the exact position. Without validation this mapping would have run silently, because an unknown transformer is skipped at evaluation, and /post/slug would have carried the unslugged title until someone noticed downstream.

Change sluggify to slugify and run check.js once more:

valid: true
errors: []
warnings: []

Now wire the check into the run itself. In main.js:

const report = mapper.validate(post)
if (!report.valid) throw new Error(JSON.stringify(report.errors))
const { valid, errors, ...result } = await mapper.map(post, {
title: 'Maps All the Way Down',
words: '1200'
})
console.log(JSON.stringify(result, null, 2))
{
"post": {
"title": "Maps All the Way Down",
"slug": "maps-all-the-way-down",
"words": 1200
}
}

The gate costs one synchronous call and can never throw on its own: validation always returns a report. Evaluation still validates the input (that’s valid and errors in the envelope); the gate ensures the mapping was never the problem.

You’ve seen the two validation positions find different classes of mistake, read diagnostics by pointer and rule, and gated evaluation on a clean report. The full report contract is on Mapping validation; the CI version of the gate is in Validate a mapping.