Skip to main content
The replacer function provides fine-grained control over TOON encoding, allowing you to transform values, filter properties, add metadata, or customize serialization behavior. It’s similar to JSON.stringify’s replacer parameter but with additional path tracking.

Overview

The replacer function is called for every value during encoding:
  • The root value (with key = '' and path = [])
  • Every object property (with property name as key)
  • Every array element (with string index as key: '0', '1', etc.)
import { encode } from '@toon-format/toon'

const data = { name: 'Alice', password: 'secret' }

const safe = encode(data, {
  replacer: (key, value) => {
    if (key === 'password') return undefined // Omit password
    return value
  }
})
// name: Alice

Function Signature

type EncodeReplacer = (
  key: string,
  value: JsonValue,
  path: readonly (string | number)[]
) => unknown
key
string
required
The property key or array index (as string). Empty string ('') for the root value.
value
JsonValue
required
The normalized JSON value at this location (after converting to JSON data model).
path
readonly (string | number)[]
required
Array representing the path from root to this value. Empty array ([]) for root.
Returns: unknown
  • Return the replacement value (will be normalized again)
  • Return undefined to omit the property/element
  • For root value, returning undefined means “no change” (root cannot be omitted)

Common Use Cases

1. Filtering Sensitive Fields

Remove passwords, tokens, or other sensitive data before encoding:
import { encode } from '@toon-format/toon'

const user = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  password: 'secret123',
  apiToken: 'tok_xyz'
}

const safe = encode(user, {
  replacer: (key, value) => {
    // Omit sensitive fields
    if (key === 'password' || key === 'apiToken') {
      return undefined
    }
    return value
  }
})
// id: 1
// name: Alice
// email: [email protected]

2. Transforming Values

Modify values during encoding (e.g., normalize strings, format numbers):
import { encode } from '@toon-format/toon'

const data = {
  status: 'active',
  description: 'User Account',
  amount: 123.456
}

const transformed = encode(data, {
  replacer: (key, value) => {
    // Uppercase all strings
    if (typeof value === 'string') {
      return value.toUpperCase()
    }
    // Round numbers to 2 decimals
    if (typeof value === 'number') {
      return Math.round(value * 100) / 100
    }
    return value
  }
})
// status: ACTIVE
// description: USER ACCOUNT
// amount: 123.46

3. Adding Metadata

Inject timestamps, version info, or other metadata at the root:
import { encode } from '@toon-format/toon'

const data = { users: [{ id: 1, name: 'Alice' }] }

const withMetadata = encode(data, {
  replacer: (key, value, path) => {
    // Add metadata to root object
    if (path.length === 0 && typeof value === 'object' && value !== null) {
      return {
        ...value as object,
        _timestamp: Date.now(),
        _version: '1.0'
      }
    }
    return value
  }
})
// _timestamp: 1709740800000
// _version: 1.0
// users[1]{id,name}:
//   1,Alice

4. Path-Based Filtering

Use the path parameter to filter based on location in the data structure:
import { encode } from '@toon-format/toon'

const data = {
  public: { name: 'Alice', email: '[email protected]' },
  private: { ssn: '123-45-6789', salary: 75000 }
}

const publicOnly = encode(data, {
  replacer: (key, value, path) => {
    // Omit entire 'private' branch
    if (path.length === 1 && path[0] === 'private') {
      return undefined
    }
    return value
  }
})
// public:
//   name: Alice
//   email: [email protected]

5. Conditional Serialization

Serialize objects differently based on context:
import { encode } from '@toon-format/toon'

const data = {
  users: [
    { id: 1, name: 'Alice', active: true, lastLogin: null },
    { id: 2, name: 'Bob', active: false, lastLogin: '2025-01-15' }
  ]
}

const filtered = encode(data, {
  replacer: (key, value, path) => {
    // Omit null values
    if (value === null) {
      return undefined
    }
    // Only include active users
    if (
      path.length === 2 &&
      path[0] === 'users' &&
      typeof value === 'object' &&
      value !== null &&
      'active' in value &&
      !value.active
    ) {
      return undefined
    }
    return value
  }
})
// users[1]{id,name,active}:
//   1,Alice,true

6. Type Conversion

Convert dates, BigInts, or other non-JSON types:
import { encode } from '@toon-format/toon'

const data = {
  created: new Date('2025-03-06'),
  count: BigInt(9007199254740991)
}

const serialized = encode(data, {
  replacer: (key, value) => {
    // Convert Date to ISO string
    if (value instanceof Date) {
      return value.toISOString()
    }
    // Convert BigInt to string
    if (typeof value === 'bigint') {
      return value.toString()
    }
    return value
  }
})
// created: 2025-03-06T00:00:00.000Z
// count: 9007199254740991
Note: The replacer receives normalized JSON values, so Date and BigInt are already converted by the time the replacer sees them. To handle these types, convert them before passing to encode().

7. Allowlist Pattern

Only include specific fields:
import { encode } from '@toon-format/toon'

const ALLOWED_FIELDS = new Set(['id', 'name', 'email'])

const user = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  password: 'secret',
  internal_id: 'x123',
  metadata: { foo: 'bar' }
}

const allowlisted = encode(user, {
  replacer: (key, value, path) => {
    // At root level, only include allowed fields
    if (path.length === 1 && !ALLOWED_FIELDS.has(key)) {
      return undefined
    }
    return value
  }
})
// id: 1
// name: Alice
// email: [email protected]

8. Truncating Large Arrays

Limit array sizes to reduce token count:
import { encode } from '@toon-format/toon'

const data = {
  items: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }))
}

const truncated = encode(data, {
  replacer: (key, value, path) => {
    // Truncate arrays to first 10 items
    if (Array.isArray(value) && value.length > 10) {
      return value.slice(0, 10)
    }
    return value
  }
})
// items[10]{id,name}:
//   0,Item 0
//   1,Item 1
//   ...
//   9,Item 9

Advanced Patterns

Composing Multiple Replacers

Combine multiple transformations:
import { encode } from '@toon-format/toon'

type Replacer = (key: string, value: unknown, path: readonly (string | number)[]) => unknown

function composeReplacers(...replacers: Replacer[]): Replacer {
  return (key, value, path) => {
    let result = value
    for (const replacer of replacers) {
      result = replacer(key, result, path)
      if (result === undefined) return undefined
    }
    return result
  }
}

const removePasswords: Replacer = (key, value) => 
  key === 'password' ? undefined : value

const uppercaseStrings: Replacer = (key, value) =>
  typeof value === 'string' ? value.toUpperCase() : value

const combined = composeReplacers(removePasswords, uppercaseStrings)

const user = { name: 'Alice', password: 'secret' }
encode(user, { replacer: combined })
// name: ALICE

Logging All Values

Debug encoding by logging every value:
import { encode } from '@toon-format/toon'

const data = { users: [{ id: 1, name: 'Alice' }] }

encode(data, {
  replacer: (key, value, path) => {
    console.log(`[${path.join('.')}] ${key}:`, value)
    return value
  }
})
// [] :
// [users] users: [ { id: 1, name: 'Alice' } ]
// [users.0] 0: { id: 1, name: 'Alice' }
// [users.0.id] id: 1
// [users.0.name] name: Alice

Path-Based Transformation

Apply different transformations based on depth:
import { encode } from '@toon-format/toon'

const data = {
  summary: { total: 100 },
  details: {
    users: [
      { id: 1, score: 95.5 },
      { id: 2, score: 87.3 }
    ]
  }
}

const encoded = encode(data, {
  replacer: (key, value, path) => {
    // Round numbers only in deeply nested structures (depth > 2)
    if (typeof value === 'number' && path.length > 2) {
      return Math.round(value)
    }
    return value
  }
})
// summary:
//   total: 100
// details:
//   users[2]{id,score}:
//     1,96
//     2,87

Performance Considerations

The replacer function is called for every value in the data structure:
const data = {
  users: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `User ${i}` }))
}

// ⚠️ Called 3001 times: 1 root + 1000 array elements + 2000 properties
encode(data, {
  replacer: (key, value) => {
    // Keep this function fast!
    return value
  }
})
Performance tips:
  • Keep replacer logic simple and fast
  • Avoid expensive operations (API calls, database queries)
  • Use early returns for common cases
  • Pre-compute lookups outside the replacer
// ✅ Good: Pre-compute lookup
const SENSITIVE_FIELDS = new Set(['password', 'apiToken', 'ssn'])

encode(data, {
  replacer: (key, value) => {
    if (SENSITIVE_FIELDS.has(key)) return undefined
    return value
  }
})

// ❌ Bad: Compute every time
encode(data, {
  replacer: (key, value) => {
    if (['password', 'apiToken', 'ssn'].includes(key)) return undefined
    return value
  }
})

Comparison with JSON.stringify

TOON’s replacer is similar to JSON.stringify’s replacer with key differences:
FeatureTOONJSON.stringify
Array elementsCalled with string index ('0', '1')Same
Root valueCalled with key = ''Same
Path tracking✅ Third parameter path❌ Not available
Root omission❌ Cannot omit root✅ Can omit root
Array support❌ Only functions✅ Functions or arrays
// JSON.stringify array replacer (not supported in TOON)
JSON.stringify(data, ['id', 'name']) // Only include these keys

// TOON equivalent (use function)
encode(data, {
  replacer: (key, value, path) => {
    if (path.length === 1 && !['id', 'name'].includes(key)) {
      return undefined
    }
    return value
  }
})

Error Handling

The replacer function should not throw errors. Return undefined to omit invalid values:
import { encode } from '@toon-format/toon'

const data = { items: [{ price: '10.50' }, { price: 'invalid' }] }

const safe = encode(data, {
  replacer: (key, value) => {
    if (key === 'price' && typeof value === 'string') {
      const num = parseFloat(value)
      // Omit invalid numbers instead of throwing
      return isNaN(num) ? undefined : num
    }
    return value
  }
})
// items[1]{price}:
//   10.5
If the replacer throws an error, encoding will fail. Use try-catch inside the replacer if needed.

Examples Repository

For more examples, see:

Build docs developers (and LLMs) love