Converter - Transformations
Transformations allow you to process and reshape data after parsing. The converter provides three powerful methods: .transform(), .optional(), and .array().
Transform Method
Section titled “Transform Method”The .transform() method applies a function to the parsed value, allowing you to modify the output.
Basic Usage
Section titled “Basic Usage”import { x } from 'stax-xml/converter';
// Parse string and transform to uppercaseconst schema = x.string() .xpath('/message') .transform(value => value.toUpperCase());
const xml = '<message>hello world</message>';const result = schema.parseSync(xml);// "HELLO WORLD"Type Transformation
Section titled “Type Transformation”Transform can change the output type:
// String to booleanconst boolSchema = x.string() .xpath('/enabled') .transform(value => value === 'true');
// XML: <enabled>true</enabled>// Result: true (boolean, not string)
// String to Dateconst dateSchema = x.string() .xpath('/publishDate') .transform(value => new Date(value));
// XML: <publishDate>2024-01-15</publishDate>// Result: Date object
// Number to string with formattingconst priceSchema = x.number() .xpath('/price') .transform(value => `$${value.toFixed(2)}`);
// XML: <price>19.99</price>// Result: "$19.99"Type Inference with Transform
Section titled “Type Inference with Transform”TypeScript infers the transformed type:
import { type Infer } from 'stax-xml/converter';
const schema = x.string() .xpath('/value') .transform(v => parseInt(v));
type Output = Infer<typeof schema>;// number (not string)
const numericSchema = x.number() .xpath('/value') .transform(v => v > 10);
type BoolOutput = Infer<typeof numericSchema>;// booleanObject Transformation
Section titled “Object Transformation”Transform entire objects to reshape data:
const personSchema = x.object({ firstName: x.string().xpath('/person/firstName'), lastName: x.string().xpath('/person/lastName'), age: x.number().xpath('/person/age')}).transform(person => ({ fullName: `${person.firstName} ${person.lastName}`, age: person.age, isAdult: person.age >= 18}));
// XML: <person><firstName>John</firstName><lastName>Doe</lastName><age>25</age></person>// Result: { fullName: "John Doe", age: 25, isAdult: true }
type Person = Infer<typeof personSchema>;// { fullName: string; age: number; isAdult: boolean; }Chaining Transforms
Section titled “Chaining Transforms”Multiple transforms can be chained:
const schema = x.string() .xpath('/value') .transform(v => v.trim()) // First: trim whitespace .transform(v => v.toLowerCase()) // Then: lowercase .transform(v => v.split(',')) // Then: split to array .transform(v => v.map(s => s.trim())); // Finally: trim each
// XML: <value> Apple, Banana, Cherry </value>// Result: ["apple", "banana", "cherry"]Use Cases
Section titled “Use Cases”1. Boolean Parsing
const activeSchema = x.string() .xpath('/user/@active') .transform(v => v === 'true' || v === '1');2. Enum Mapping
type Status = 'active' | 'inactive' | 'pending';
const statusSchema = x.string() .xpath('/status') .transform(v => v.toLowerCase() as Status);3. Data Cleanup
const emailSchema = x.string() .xpath('/email') .transform(v => v.trim().toLowerCase());4. Calculations
const totalSchema = x.object({ price: x.number().xpath('/price'), quantity: x.number().xpath('/quantity'), tax: x.number().xpath('/tax')}).transform(item => ({ ...item, total: (item.price * item.quantity) * (1 + item.tax)}));5. URL Normalization
const urlSchema = x.string() .xpath('/link') .transform(url => { if (!url.startsWith('http')) { return `https://${url}`; } return url; });Optional Method
Section titled “Optional Method”The .optional() method makes a field return undefined instead of throwing an error when parsing fails.
Basic Usage
Section titled “Basic Usage”const schema = x.string() .xpath('/missing') .optional();
const result = schema.parseSync('<root></root>');// undefined (not empty string)With Type Inference
Section titled “With Type Inference”import { type Infer } from 'stax-xml/converter';
const schema = x.number() .xpath('/value') .optional();
type Output = Infer<typeof schema>;// number | undefinedIn Objects
Section titled “In Objects”Optional is particularly useful in object schemas:
const userSchema = x.object({ id: x.number().xpath('/user/id'), // Required username: x.string().xpath('/user/username'), // Required email: x.string().xpath('/user/email').optional(), // Optional phone: x.string().xpath('/user/phone').optional() // Optional});
// All of these are valid:userSchema.parseSync('<user><id>1</id><username>john</username></user>');// { id: 1, username: "john", email: undefined, phone: undefined }
userSchema.parseSync('<user><id>1</id><username>john</username><email>j@example.com</email></user>');// { id: 1, username: "john", email: "j@example.com", phone: undefined }With Validation
Section titled “With Validation”Optional works with validation - validation only runs if value exists:
const schema = x.number() .xpath('/age') .min(0) .max(120) .optional();
schema.parseSync('<root></root>'); // ✅ undefinedschema.parseSync('<root><age>25</age></root>'); // ✅ 25schema.parseSync('<root><age>150</age></root>'); // ❌ Error: greater than maxOptional vs Empty
Section titled “Optional vs Empty”Without optional:
x.string().xpath('/value').parseSync('<root></root>');// "" (empty string)
x.number().xpath('/value').parseSync('<root></root>');// NaNWith optional:
x.string().xpath('/value').optional().parseSync('<root></root>');// undefined
x.number().xpath('/value').optional().parseSync('<root></root>');// undefinedOptional with Transform
Section titled “Optional with Transform”Optional can be combined with transform:
// Transform only if value existsconst schema = x.string() .xpath('/date') .optional() .transform(v => v ? new Date(v) : undefined);
// Or with default valueconst withDefault = x.string() .xpath('/value') .optional() .transform(v => v ?? 'default');Use Cases
Section titled “Use Cases”1. Optional Configuration
const config = x.object({ host: x.string().xpath('/config/host'), port: x.number().xpath('/config/port').optional(), // Defaults available ssl: x.string().xpath('/config/ssl').optional(), timeout: x.number().xpath('/config/timeout').optional()});2. Partial User Data
const user = x.object({ id: x.number().xpath('/user/@id'), name: x.string().xpath('/user/name'), bio: x.string().xpath('/user/bio').optional(), avatar: x.string().xpath('/user/avatar').optional(), website: x.string().xpath('/user/website').optional()});3. Feature Flags
const features = x.object({ analytics: x.string().xpath('/features/analytics').optional().transform(v => v === 'true'), darkMode: x.string().xpath('/features/darkMode').optional().transform(v => v === 'true'), beta: x.string().xpath('/features/beta').optional().transform(v => v === 'true')});Array Method
Section titled “Array Method”The .array() method converts any schema into an array schema.
Basic Usage
Section titled “Basic Usage”// Convert string schema to arrayconst stringSchema = x.string();const arraySchema = stringSchema.array('//item');
const xml = '<root><item>A</item><item>B</item><item>C</item></root>';const result = arraySchema.parseSync(xml);// ["A", "B", "C"]
// Direct creation (equivalent)const directArray = x.array(x.string(), '//item');With Object Schemas
Section titled “With Object Schemas”const bookSchema = x.object({ title: x.string().xpath('./title'), author: x.string().xpath('./author')});
// Convert to arrayconst booksArray = bookSchema.array('//book');
// Or use x.array()const booksArray2 = x.array(bookSchema, '//book');Type Inference
Section titled “Type Inference”const itemSchema = x.object({ id: x.number().xpath('./id'), name: x.string().xpath('./name')});
const arraySchema = itemSchema.array('//item');
type Items = Infer<typeof arraySchema>;// Array<{ id: number; name: string; }>
// Get single item typetype Item = Infer<typeof arraySchema>[number];// { id: number; name: string; }With Transform
Section titled “With Transform”Transform can be applied before or after array conversion:
// Transform each elementconst transformed = x.string() .transform(v => v.toUpperCase()) .array('//item');
// XML: <root><item>a</item><item>b</item></root>// Result: ["A", "B"]
// Transform the entire arrayconst arrayTransformed = x.string() .array('//item') .transform(arr => arr.filter(s => s.length > 0));With Optional
Section titled “With Optional”// Optional array (undefined if no elements)const optionalArray = x.string() .array('//item') .optional();
// Empty array vs undefinedx.array(x.string(), '//item').parseSync('<root></root>');// [] (empty array)
x.array(x.string(), '//item').optional().parseSync('<root></root>');// [] (still empty array, optional doesn't change this)
// Use transform for undefined on emptyx.array(x.string(), '//item') .transform(arr => arr.length === 0 ? undefined : arr);Method Chaining
Section titled “Method Chaining”All transformation methods return new schema instances and can be chained.
Chaining Order Matters
Section titled “Chaining Order Matters”// Transform then make optionalconst schema1 = x.string() .xpath('/value') .transform(v => v.toUpperCase()) .optional();
// Make optional then transformconst schema2 = x.string() .xpath('/value') .optional() .transform(v => v ? v.toUpperCase() : undefined);
// Both work, but handle missing values differentlyComplex Chaining
Section titled “Complex Chaining”const schema = x.string() .xpath('/data') .transform(v => v.trim()) // Clean whitespace .transform(v => v.toLowerCase()) // Lowercase .optional() // Make optional .array('//data') // Convert to array .transform(arr => [...new Set(arr)]); // Remove duplicates
// XML: <root><data> APPLE </data><data> Banana </data><data> APPLE </data></root>// Result: ["apple", "banana"]Real-World Example
Section titled “Real-World Example”// Parse and process product dataconst productSchema = x.object({ id: x.number().xpath('./@id').int(), name: x.string().xpath('./name').transform(v => v.trim()), price: x.number().xpath('./price').min(0), discount: x.number().xpath('./discount').optional(), tags: x.array( x.string().transform(v => v.toLowerCase()), './tag' ).optional(), inStock: x.string() .xpath('./@inStock') .transform(v => v === 'true')}).transform(product => ({ ...product, finalPrice: product.discount ? product.price * (1 - product.discount / 100) : product.price, tags: product.tags ?? []}));
type Product = Infer<typeof productSchema>;// {// id: number;// name: string;// price: number;// discount: number | undefined;// tags: string[];// inStock: boolean;// finalPrice: number;// }Best Practices
Section titled “Best Practices”1. Transform for Type Conversion
Section titled “1. Transform for Type Conversion”// ✅ Use transform for type changesconst bool = x.string().xpath('/flag').transform(v => v === 'true');const date = x.string().xpath('/date').transform(v => new Date(v));
// ❌ Don't do type conversion in codeconst str = x.string().xpath('/flag');const result = str.parseSync(xml);const bool = result === 'true'; // Do this in transform instead2. Optional for Missing Fields
Section titled “2. Optional for Missing Fields”// ✅ Use optional for genuinely optional fieldsconst user = x.object({ id: x.number().xpath('/id'), bio: x.string().xpath('/bio').optional()});
// ❌ Don't use optional for required fieldsconst user = x.object({ id: x.number().xpath('/id').optional() // ID should be required!});3. Validate Before Transform
Section titled “3. Validate Before Transform”// ✅ Validation before transformationconst age = x.number() .xpath('/age') .min(0) .max(120) .transform(age => age >= 18 ? 'adult' : 'minor');
// Validation catches invalid data before transform runs4. Keep Transforms Simple
Section titled “4. Keep Transforms Simple”// ✅ Simple, clear transformsconst upperCase = x.string() .xpath('/value') .transform(v => v.toUpperCase());
// ❌ Complex logic in transforms (hard to test/maintain)const complex = x.string() .xpath('/value') .transform(v => { if (v.includes('special')) { return v.split(',').map(x => x.trim()).filter(Boolean).join(';'); } else { return v.toUpperCase().replace(/\s+/g, '_'); } });
// Better: Break into multiple transforms or handle in code5. Use Optional with Defaults
Section titled “5. Use Optional with Defaults”// ✅ Provide defaults for optional valuesconst config = x.object({ port: x.number().xpath('/port').optional().transform(v => v ?? 8080), host: x.string().xpath('/host').optional().transform(v => v ?? 'localhost')});Common Patterns
Section titled “Common Patterns”Parse Comma-Separated Lists
Section titled “Parse Comma-Separated Lists”const tags = x.string() .xpath('/tags') .transform(v => v.split(',').map(s => s.trim()));
// XML: <tags>apple, banana, cherry</tags>// Result: ["apple", "banana", "cherry"]Parse Boolean Variations
Section titled “Parse Boolean Variations”const bool = x.string() .xpath('/enabled') .transform(v => { const normalized = v.toLowerCase(); return normalized === 'true' || normalized === '1' || normalized === 'yes'; });Calculate Totals
Section titled “Calculate Totals”const order = x.object({ items: x.array( x.object({ price: x.number().xpath('./price'), quantity: x.number().xpath('./quantity') }), '//item' )}).transform(order => ({ items: order.items, subtotal: order.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)}));Normalize URLs
Section titled “Normalize URLs”const link = x.string() .xpath('//@href') .transform(url => { if (url.startsWith('//')) return `https:${url}`; if (url.startsWith('/')) return `https://example.com${url}`; if (!url.startsWith('http')) return `https://${url}`; return url; });Parse Nested JSON Strings
Section titled “Parse Nested JSON Strings”const metadata = x.string() .xpath('/metadata') .transform(v => JSON.parse(v));
// XML: <metadata>{"key": "value"}</metadata>// Result: { key: "value" }Next Steps
Section titled “Next Steps”- Learn about Writing XML for serialization
- See Examples for real-world transform usage
- Review Schema Types for validation options
- Check Core Concepts for fundamentals