value.cast, value.clean, and value.convert
Schema-aware instance methods on jt.value. All three operate against the registry and require enableTypeCast: true in JsonTology.create options for type coercion to work.
value.cast
Declaration. Coerces types (e.g. "9.99" → 9.99, "true" → true) and applies schema default values. Requires enableTypeCast: true. Throws CoercionError when the coerced data fails validation.
Use this when ingesting data from sources that serialize numbers and booleans as strings - CSV imports, URL query parameters, HTML form submissions, application/x-www-form-urlencoded bodies.
Don't use this when the source data is already properly typed (use instantiate instead). Don't use cast when you want type coercion but not defaults (use convert).
Examples
Example 1: Cast form input with string numbers
/**
* value.cast / clean / convert — Example 1: Schema-aware value operations
* Demonstrates: type coercion, unknown stripping, convert without defaults
*
* Operates against the canonical bookstore registry. The book is
* Hermann Hesse's Siddhartha (Suhrkamp, 1922) — a literary sibling to
* the Neverending Story rare-book fixture in Coreander's antiquariat.
*/
import {
BookSchema, bookstoreEntities
} from '../bookstore/index.js';
// cast coerces types and fills defaults.
const casted = bookstoreEntities.value.cast(BookSchema.$id, {
'authors': ['Hermann Hesse'],
'inStock': true,
'isbn': '9783518366820',
'price': {
'amount': 12,
'currency': 'EUR'
},
'printStatus': 'inPrint',
'title': 'Siddhartha'
}) as Record<string, unknown>;
console.assert((casted as { 'price': { 'amount': number } }).price.amount === 12);
// clean strips unknown properties.
const dirty = {
'_cacheKey': 'k:9783518366820',
'_internalId': 'int-001',
'authors': ['Hermann Hesse'],
'isbn': '9783518366820',
'price': {
'amount': 12,
'currency': 'EUR'
},
'printStatus': 'inPrint',
'title': 'Siddhartha'
};
const cleaned = bookstoreEntities.value.clean(BookSchema.$id, dirty) as Record<string, unknown>;
console.assert(!('_internalId' in (cleaned as object)));
console.assert(!('_cacheKey' in (cleaned as object)));
// convert coerces types only — no defaults applied.
const converted = bookstoreEntities.value.convert(BookSchema.$id, {
'authors': ['Hermann Hesse'],
'isbn': '9783518366820',
'price': {
'amount': 12,
'currency': 'EUR'
},
'printStatus': 'inPrint',
'title': 'Siddhartha'
}) as Record<string, unknown>;
console.assert(typeof (converted as { 'price': { 'amount': number } }).price.amount === 'number');
console.log('cast price.amount:', (casted as { 'price': { 'amount': number } }).price.amount);
console.log('clean stripped _internalId:', !('_internalId' in cleaned));
console.log('clean stripped _cacheKey:', !('_cacheKey' in cleaned));
console.log('convert price.amount type:', typeof (converted as { 'price': { 'amount': number } }).price.amount);
Example 2: Cast URL query params for a Review filter
/**
* value.cast — Example 2: Cast URL query params for a Review filter
* Demonstrates: string-coerced rating '4' → 4 (number), schema defaults applied
*
* Query parameters arrive as strings from the URL. value.cast coerces
* compatible types automatically when enableTypeCast is active. The
* canonical Bastian Balthazar Bux review fixture provides the base data.
*/
import { JsonTology } from '../../../src/index.js';
import {
aboxFixtures, bookstoreSchemas, ReviewSchema
} from '../bookstore/index.js';
// A registry with enableTypeCast active.
// bookstoreSchemas seeds all transitive $refs (IsbnSchema, CustomerIdSchema,
// Iso8601Schema, RatingScoreSchema, ReviewIdSchema, etc.) so ReviewSchema's
// references resolve correctly.
const castEntities = JsonTology.create({
'baseIri': 'https://bookstore.example',
'enableTypeCast': true,
'schemas': bookstoreSchemas
});
// string from query param — coerced to 4 (number)
// Simulate req.query.rating = '4' (a string from the URL).
const params = castEntities.value.cast(ReviewSchema.$id, {
'body': aboxFixtures.review.body,
'bookIsbn': aboxFixtures.review.bookIsbn,
'customerId': aboxFixtures.review.customerId,
'postedAt': aboxFixtures.review.postedAt,
'rating': '4',
'reviewId': aboxFixtures.review.reviewId
}) as Record<string, unknown>;
console.assert((params as { 'rating': number }).rating === 4);
console.assert(typeof (params as { 'rating': number }).rating === 'number');
console.log('query param "4" cast to:', (params as { 'rating': number }).rating, typeof (params as { 'rating': number }).rating);
value.clean
Declaration. Strips properties not declared in the schema from the data. Throws CoercionError when the cleaned data fails validation.
Use this when you need to sanitize data that may carry extra properties not in the schema - for example, third-party API responses, database rows with extra columns, or enriched records that need to be reduced before forwarding.
Don't use this when you want defaults to be applied too (use instantiate which does both). Use clean when you specifically want only stripping, no defaults.
Examples
Example 1: Strip internal fields from an API response
/**
* value.clean — Example 1: Strip internal fields from an API response
* Demonstrates: unknown properties removed, declared fields preserved
*
* An API response for Michael Ende's Die unendliche Geschichte carries
* internal cache and ID fields not declared in BookSchema. value.clean
* strips them while keeping all declared fields intact.
*/
import {
BookSchema, bookstoreEntities
} from '../bookstore/index.js';
import { aboxFixtures } from '../bookstore/index.js';
const apiResponse = {
// not in BookSchema
'_cacheKey': 'k:9783522128001',
// not in BookSchema
'_internalId': 'int-001',
'authors': aboxFixtures.rareBook.authors,
'inStock': aboxFixtures.rareBook.inStock,
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'printStatus': aboxFixtures.rareBook.printStatus,
'title': aboxFixtures.rareBook.title
};
const cleaned = bookstoreEntities.value.clean(BookSchema.$id, apiResponse) as Record<string, unknown>;
// Internal fields are gone.
console.assert(!('_internalId' in (cleaned as object)));
console.assert(!('_cacheKey' in (cleaned as object)));
// Declared fields are preserved.
console.assert((cleaned as { 'isbn': string }).isbn === aboxFixtures.rareBook.isbn);
console.assert((cleaned as { 'title': string }).title === aboxFixtures.rareBook.title);
console.log('_internalId removed:', !('_internalId' in cleaned));
console.log('_cacheKey removed:', !('_cacheKey' in cleaned));
console.log('isbn preserved:', (cleaned as { 'isbn': string }).isbn);
console.log('title preserved:', (cleaned as { 'title': string }).title);
value.convert
Declaration. Coerces types without applying schema default values. Requires enableTypeCast: true. Throws CoercionError when the data fails validation after type conversion.
Use this when you want type coercion but explicitly want to control which defaults are applied. Contrast: cast = coerce types + fill defaults; convert = coerce types only; instantiate = coerce types + fill defaults + strip unknowns + run transforms.
Examples
Example 1: Convert types for a partial review without filling defaults
/**
* value.convert — Example 1: Convert types without filling defaults
* Demonstrates: string '5' → number 5, no schema defaults applied
*
* A review submission arrives with rating as a string. value.convert coerces
* the type without applying schema defaults. The canonical Bastian Balthazar
* Bux review of the 1979 Thienemann Neverending Story first edition provides
* the fixture data.
*/
import { JsonTology } from '../../../src/index.js';
import {
aboxFixtures, bookstoreSchemas, ReviewSchema
} from '../bookstore/index.js';
// bookstoreSchemas seeds all transitive $refs so ReviewSchema's references
// (IsbnSchema, CustomerIdSchema, Iso8601Schema, etc.) resolve correctly.
const castEntities = JsonTology.create({
'baseIri': 'https://bookstore.example',
'enableTypeCast': true,
'schemas': bookstoreSchemas
});
// string coerced to number — no defaults applied
const converted = castEntities.value.convert(ReviewSchema.$id, {
'body': aboxFixtures.review.body,
'bookIsbn': aboxFixtures.review.bookIsbn,
'customerId': aboxFixtures.review.customerId,
'postedAt': aboxFixtures.review.postedAt,
'rating': '5',
'reviewId': aboxFixtures.review.reviewId
}) as Record<string, unknown>;
console.assert((converted as { 'rating': number }).rating === 5);
console.assert(typeof (converted as { 'rating': number }).rating === 'number');
console.log('string "5" converted to:', (converted as { 'rating': number }).rating, typeof (converted as { 'rating': number }).rating);
Comparison
cast fills defaults, clean strips unknown properties, convert coerces types only. Other libraries rarely separate these three concerns; each tab notes how the library maps onto them.
const jt = JsonTology.create({ ..., enableTypeCast: true });
const book = jt.value.cast(BookSchema.$id, rawData); // cast: coerce + fill defaults
const cleaned = jt.value.clean(BookSchema.$id, data); // clean: strip unknown properties
const partial = jt.value.convert(BookSchema.$id, rawData); // convert: coerce only, no defaults// cast — .coerce wrappers per-field, defaults filled by Zod's own .default():
const BookSchema = z.object({
price: z.coerce.number(),
inStock: z.coerce.boolean(),
});
const book = BookSchema.parse(rawData);
// clean — .parse() strips unknown keys by default (same mechanism as cast above):
const cleaned = BookSchema.parse(data);
// convert — Limitation: no built-in way to coerce without also applying
// .default() on the same schema; requires a second schema without defaults.import * as v from 'valibot';
// cast — Limitation: no schema-wide cast option. Wrap each coerced field
// individually and rebuild the schema:
const BookSchema = v.object({
price: v.pipe(v.unknown(), v.transform(Number), v.number()),
inStock: v.pipe(v.unknown(), v.transform(Boolean), v.boolean()),
});
const book = v.parse(BookSchema, rawData);
// clean — v.object() strips unknown keys by default during v.parse:
const cleaned = v.parse(BookSchema, data);
// Use v.looseObject() to preserve unknowns; v.strictObject() to reject them.
// convert — same Limitation as cast: no schema-wide option to coerce
// without a separate defaults step.import * as t from 'io-ts';
// cast — Limitation: io-ts has no schema-wide coercion. Build a custom codec
// for each field that needs to coerce string → number/boolean:
const NumberFromString = new t.Type<number, string, unknown>(
'NumberFromString',
(input): input is number => typeof input === 'number',
(input, ctx) => {
const parsed = typeof input === 'string' ? Number(input) : input;
return typeof parsed === 'number' && !Number.isNaN(parsed)
? t.success(parsed) : t.failure(input, ctx);
},
(output) => String(output),
);
const BookCodec = t.type({ price: NumberFromString /* ... */ });
const decoded = BookCodec.decode(rawData);
// clean — t.type accepts unknown extra properties; use t.exact to strip them:
const StrictBookCodec = t.exact(t.type({
isbn: t.string,
title: t.string,
authors: t.array(t.string),
price: t.number,
}));
const result = StrictBookCodec.decode(data); // unknown keys removed in result.right
// convert — same custom-codec approach as cast, minus any default-filling logic.import { Value } from '@sinclair/typebox/value';
const book = Value.Convert(BookSchema, rawData); // cast: type conversion
Value.Clean(BookSchema, Value.Clone(data)); // clean: removes additional properties
const partial = Value.Convert(PartialSchema, rawData); // convert: same call against a schema with no defaults// cast — { coerceTypes: true } combined with AJV's useDefaults:
const ajv = new Ajv({ coerceTypes: true, useDefaults: true });
ajv.validate(bookSchema, rawData); // rawData mutated in place
// clean — { removeAdditional: true }:
const ajvClean = new Ajv({ removeAdditional: true });
ajvClean.validate(bookSchema, data); // mutates data in place
// convert — { coerceTypes: true } alone, useDefaults omitted:
const ajvConvert = new Ajv({ coerceTypes: true });
ajvConvert.validate(bookSchema, rawData);# cast — Pydantic v2 coerces compatible types and fills defaults together
# (strict=False is the default); the two are not separable:
book = Book.model_validate(raw_data) # '14.99' → 14.99
# clean — extra fields are ignored by default (model_config extra='ignore'):
cleaned = Book.model_validate(data)
# convert — Limitation: no way to coerce types without also filling
# defaults on the same model.// Limitation: cast, clean, and convert are not directly supported as
// separate operations in these libraries. See /comparisons for the matrix.Related
JsonTology.instantiate- validate + apply defaults + strip unknowns + run transformsvalue.create- synthesize a zero-value blank instance
See also
- Bookstore domain - where
Book,Revieware defined