Model Codecs
Description
Section titled “Description”This example demonstrates how to use model codecs for encoding/decoding complex object structures with field metadata. Topics covered:
- ObjectModelCodec for encoding/decoding typed objects
- Defining field metadata with FieldMetadata type
- Encoding format options: ‘json’ vs ‘msgpack’
- PrimitiveModelCodec for simple value types
- ArrayModelCodec for array model types
- Handling optional fields
- Field renaming with wireKey vs property name
- Round-trip encoding with ObjectModelCodec
Prerequisites
Section titled “Prerequisites”- No LocalNet required
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example common/11-model-codecs.ts/** * Example: Model Codecs * * This example demonstrates how to use model codecs for encoding/decoding * complex object structures with field metadata. * * Topics covered: * - ObjectModelCodec for encoding/decoding typed objects * - Defining field metadata with FieldMetadata type * - Encoding format options: 'json' vs 'msgpack' * - PrimitiveModelCodec for simple value types * - ArrayModelCodec for array model types * - Handling optional fields * - Field renaming with wireKey vs property name * - Round-trip encoding with ObjectModelCodec * * Prerequisites: * - No LocalNet required */
import type { ArrayModelMetadata, EncodingFormat, FieldMetadata, ObjectModelMetadata, PrimitiveModelMetadata,} from '@algorandfoundation/algokit-utils/common'import { // Utilities Address, addressCodec, // Composite codecs ArrayCodec, arrayEqual, ArrayModelCodec, bigIntCodec, bytesCodec, // Primitive codecs (for field definitions) numberCodec, // Model codecs ObjectModelCodec, PrimitiveModelCodec, stringCodec,} from '@algorandfoundation/algokit-utils/common'import { formatHex, printHeader, printInfo, printStep, printSuccess } from '../shared/utils.js'
// ============================================================================// Main Example// ============================================================================
printHeader('Model Codecs Example')
// ============================================================================// Step 1: Introduction to Model Codecs// ============================================================================printStep(1, 'Introduction to Model Codecs')
printInfo('Model codecs provide structured encoding/decoding for complex objects.')printInfo('')printInfo('Three model codec types:')printInfo(' ObjectModelCodec - Encodes/decodes typed objects with field metadata')printInfo(' PrimitiveModelCodec - Wraps primitive codecs with model metadata')printInfo(' ArrayModelCodec - Wraps array codecs with model metadata')printInfo('')printInfo('Key features:')printInfo(' - Field renaming: map property names to wire keys (e.g., "name" -> "n")')printInfo(' - Optional fields: fields can be marked optional and omitted when empty')printInfo(' - Nested objects: ObjectModelCodec can contain other ObjectModelCodecs')printInfo(' - Format support: works with both "json" and "msgpack" formats')printInfo('')printSuccess('Model codecs provide structured serialization for domain objects')
// ============================================================================// Step 2: FieldMetadata Type - Defining Object Fields// ============================================================================printStep(2, 'FieldMetadata Type - Defining Object Fields')
printInfo('FieldMetadata defines how each field in an object is encoded/decoded.')printInfo('')printInfo('FieldMetadata interface:')printInfo(' {')printInfo(' name: string // Property name in the TypeScript object')printInfo(' wireKey?: string // Key used in wire format (defaults to name)')printInfo(' codec: Codec // Codec for encoding/decoding the field value')printInfo(' optional: boolean // Whether the field can be omitted')printInfo(' flattened?: boolean // Merge nested object fields into parent')printInfo(' }')printInfo('')
// Example field metadataconst nameField: FieldMetadata = { name: 'name', wireKey: 'n', // Encode as "n" instead of "name" codec: stringCodec, optional: false,}
const ageField: FieldMetadata = { name: 'age', wireKey: 'a', codec: numberCodec, optional: true, // Can be omitted if not set}
printInfo('Example field definitions:')printInfo(` nameField: { name: "name", wireKey: "n", optional: false }`)printInfo(` ageField: { name: "age", wireKey: "a", optional: true }`)printInfo('')printSuccess('FieldMetadata controls how individual fields are serialized')
// ============================================================================// Step 3: ObjectModelCodec - Basic Usage// ============================================================================printStep(3, 'ObjectModelCodec - Basic Usage')
printInfo('ObjectModelCodec encodes/decodes typed objects using field metadata.')printInfo('')
// Define a simple Person typetype Person = { name: string age?: number email?: string}
// Define metadata for Personconst personMetadata: ObjectModelMetadata<Person> = { name: 'Person', kind: 'object', fields: [ { name: 'name', wireKey: 'n', codec: stringCodec, optional: false }, { name: 'age', wireKey: 'a', codec: numberCodec, optional: true }, { name: 'email', wireKey: 'e', codec: stringCodec, optional: true }, ],}
// Create the codecconst personCodec = new ObjectModelCodec(personMetadata)
// Encode and decode a person
printInfo('Person type: { name: string, age?: number, email?: string }')printInfo('')printInfo(`Original: ${JSON.stringify(alice)}`)
const aliceEncodedJson = personCodec.encode(alice, 'json')const aliceEncodedMsgpack = personCodec.encode(alice, 'msgpack')
printInfo(`JSON encoded: ${JSON.stringify(aliceEncodedJson)}`)printInfo(` Note: property "name" encoded as "n", "age" as "a", "email" as "e"`)printInfo(`msgpack encoded: ${JSON.stringify(aliceEncodedMsgpack)} (same structure)`)
const aliceDecodedJson = personCodec.decode(aliceEncodedJson, 'json')const aliceDecodedMsgpack = personCodec.decode(aliceEncodedMsgpack, 'msgpack')
printInfo(`JSON decoded: ${JSON.stringify(aliceDecodedJson)}`)printInfo(`msgpack decoded: ${JSON.stringify(aliceDecodedMsgpack)}`)printInfo('')printSuccess('ObjectModelCodec handles property renaming via wireKey')
// ============================================================================// Step 4: Handling Optional Fields// ============================================================================printStep(4, 'Handling Optional Fields')
printInfo('Optional fields are omitted when undefined or at default values.')printInfo('')
// Person with only required fieldsconst bob: Person = { name: 'Bob' }
printInfo(`Person with optional fields missing: ${JSON.stringify(bob)}`)
const bobEncoded = personCodec.encode(bob, 'json')printInfo(`Encoded: ${JSON.stringify(bobEncoded)}`)printInfo(' Note: age and email are not included in output')
const bobDecoded = personCodec.decode(bobEncoded, 'json')printInfo(`Decoded: ${JSON.stringify(bobDecoded)}`)printInfo(' Note: decoded object only has "name", optional fields stay undefined')printInfo('')
// Person with default valuesconst charlie: Person = { name: '', age: 0, email: '' }
printInfo(`Person with all default values: ${JSON.stringify(charlie)}`)
const charlieEncoded = personCodec.encode(charlie, 'json')printInfo(`Encoded: ${JSON.stringify(charlieEncoded)}`)printInfo(' Note: empty object - all fields at defaults are omitted')
const charlieDecoded = personCodec.decode(charlieEncoded, 'json')printInfo(`Decoded: ${JSON.stringify(charlieDecoded)}`)printInfo('')
// Default value demonstrationprintInfo(`Codec default value: ${JSON.stringify(personCodec.defaultValue())}`)printInfo(' Required fields (name) get their codec default (empty string)')printInfo(' Optional fields are not present in the default object')printInfo('')printSuccess('Optional fields are omitted when empty or at default values')
// ============================================================================// Step 5: encodeOptional vs encode// ============================================================================printStep(5, 'encodeOptional vs encode')
printInfo('encode() always returns a value, encodeOptional() can return undefined.')printInfo('')
const allDefaults: Person = { name: '', age: 0, email: '' }
printInfo(`Object with all defaults: ${JSON.stringify(allDefaults)}`)
const encodedAlways = personCodec.encode(allDefaults, 'json')const encodedOptional = personCodec.encodeOptional(allDefaults, 'json')
printInfo(`encode(): ${JSON.stringify(encodedAlways)}`)printInfo(`encodeOptional(): ${encodedOptional === undefined ? 'undefined' : JSON.stringify(encodedOptional)}`)printInfo('')printInfo('encodeOptional() is useful for nested objects that should be')printInfo('completely omitted when all their fields are at default values.')printInfo('')printSuccess('encodeOptional() returns undefined for all-default objects')
// ============================================================================// Step 6: Encoding Format Options - JSON vs msgpack// ============================================================================printStep(6, 'Encoding Format Options - JSON vs msgpack')
printInfo('Both formats produce the same logical structure, but differ in output.')printInfo('')
// Define a type with bytes for format comparisontype AssetInfo = { assetId: bigint name: string creator: Address metadata?: Uint8Array}
const assetMetadata: ObjectModelMetadata<AssetInfo> = { name: 'AssetInfo', kind: 'object', fields: [ { name: 'assetId', wireKey: 'aid', codec: bigIntCodec, optional: false }, { name: 'name', wireKey: 'nm', codec: stringCodec, optional: false }, { name: 'creator', wireKey: 'cr', codec: addressCodec, optional: false }, { name: 'metadata', wireKey: 'md', codec: bytesCodec, optional: true }, ],}
const assetCodec = new ObjectModelCodec(assetMetadata)
const testAsset: AssetInfo = { assetId: 12345n, name: 'Test Asset', creator: Address.zeroAddress(), metadata: new Uint8Array([0x01, 0x02, 0x03]),}
printInfo(`Original asset: assetId=${testAsset.assetId}n, name="${testAsset.name}"`)printInfo(` creator=${testAsset.creator.toString().slice(0, 12)}...`)printInfo(` metadata=${formatHex(testAsset.metadata!)}`)printInfo('')
const assetEncodedJson = assetCodec.encode(testAsset, 'json')const assetJsonObj = assetEncodedJson as Record<string, unknown>printInfo(`JSON encoded:`)printInfo(` { aid: "${assetJsonObj.aid}", nm: "${assetJsonObj.nm}", cr: "${String(assetJsonObj.cr).slice(0, 12)}...", md: "${assetJsonObj.md}" }`)printInfo(' Note: bigint as string, address as string, bytes as base64')printInfo('')
const assetEncodedMsgpack = assetCodec.encode(testAsset, 'msgpack')const assetMsgpackObj = assetEncodedMsgpack as Record<string, unknown>printInfo(`msgpack encoded (preserves native types):`)printInfo(` { aid: ${assetMsgpackObj.aid}n, nm: "${assetMsgpackObj.nm}", cr: Address, md: Uint8Array }`)printInfo('')
// Decode and verifyconst assetDecodedJson = assetCodec.decode(assetEncodedJson, 'json')const assetDecodedMsgpack = assetCodec.decode(assetEncodedMsgpack, 'msgpack')
printInfo(`JSON decoded: assetId=${assetDecodedJson.assetId}n, name="${assetDecodedJson.name}"`)printInfo(`msgpack decoded: assetId=${assetDecodedMsgpack.assetId}n, name="${assetDecodedMsgpack.name}"`)printInfo('')printSuccess('Both formats preserve data with format-appropriate representations')
// ============================================================================// Step 7: PrimitiveModelCodec - Wrapping Primitive Types// ============================================================================printStep(7, 'PrimitiveModelCodec - Wrapping Primitive Types')
printInfo('PrimitiveModelCodec wraps a primitive codec with model metadata.')printInfo('Useful for creating named type wrappers around primitive values.')printInfo('')
// Define a primitive model for account balanceconst balanceMetadata: PrimitiveModelMetadata = { name: 'Balance', kind: 'primitive', codec: bigIntCodec,}
const balanceCodec = new PrimitiveModelCodec<bigint, string | bigint>(balanceMetadata)
// Use the codecconst balance = 1000000n // 1 Algo in microAlgos
printInfo(`Balance type wraps bigint with named metadata`)printInfo(` Original: ${balance}n (microAlgos)`)
const balanceEncodedJson = balanceCodec.encode(balance, 'json')const balanceEncodedMsgpack = balanceCodec.encode(balance, 'msgpack')
printInfo(` JSON encoded: "${balanceEncodedJson}" (string representation)`)printInfo(` msgpack encoded: ${balanceEncodedMsgpack}n (bigint preserved)`)
const balanceDecodedJson = balanceCodec.decode(balanceEncodedJson, 'json')const balanceDecodedMsgpack = balanceCodec.decode(balanceEncodedMsgpack, 'msgpack')
printInfo(` JSON decoded: ${balanceDecodedJson}n`)printInfo(` msgpack decoded: ${balanceDecodedMsgpack}n`)printInfo('')
// Default valueprintInfo(` Default value: ${balanceCodec.defaultValue()}n`)printInfo('')
// String model exampleconst nameModelMetadata: PrimitiveModelMetadata = { name: 'AssetName', kind: 'primitive', codec: stringCodec,}
const assetNameCodec = new PrimitiveModelCodec<string>(nameModelMetadata)
printInfo('AssetName type wraps string with named metadata')printInfo(` Default value: "${assetNameCodec.defaultValue()}" (empty string)`)printInfo('')printSuccess('PrimitiveModelCodec adds type semantics to primitive values')
// ============================================================================// Step 8: ArrayModelCodec - Wrapping Array Types// ============================================================================printStep(8, 'ArrayModelCodec - Wrapping Array Types')
printInfo('ArrayModelCodec wraps an ArrayCodec with model metadata.')printInfo('Useful for defining typed array models.')printInfo('')
// Define an array model for a list of addressesconst addressListMetadata: ArrayModelMetadata = { name: 'AddressList', kind: 'array', codec: new ArrayCodec(addressCodec),}
const addressListCodec = new ArrayModelCodec<Address[]>(addressListMetadata)
// Use the codecconst addresses = [ Address.zeroAddress(), new Address(new Uint8Array(32).fill(0x11)), new Address(new Uint8Array(32).fill(0x22)),]
printInfo('AddressList type wraps Address[] with named metadata')printInfo(` Original: ${addresses.length} addresses`)for (const addr of addresses) { printInfo(` ${addr.toString().slice(0, 20)}...`)}
const addrListEncoded = addressListCodec.encode(addresses, 'json')printInfo(` Encoded (${(addrListEncoded as unknown[]).length} elements)`)
const addrListDecoded = addressListCodec.decode(addrListEncoded, 'json')printInfo(` Decoded: ${addrListDecoded.length} addresses`)printInfo(` Match: ${addresses.every((a, i) => a.equals(addrListDecoded[i]))}`)printInfo('')
// Number array modelconst scoresMetadata: ArrayModelMetadata = { name: 'ScoreList', kind: 'array', codec: new ArrayCodec(numberCodec),}
const scoresCodec = new ArrayModelCodec<number[]>(scoresMetadata)
const scores = [95, 88, 92, 100]printInfo('ScoreList type wraps number[] with named metadata')printInfo(` Original: [${scores.join(', ')}]`)
const scoresEncoded = scoresCodec.encode(scores, 'json')const scoresDecoded = scoresCodec.decode(scoresEncoded, 'json')
printInfo(` Decoded: [${scoresDecoded.join(', ')}]`)printInfo(` Default value: [${scoresCodec.defaultValue().join(', ')}] (empty array)`)printInfo('')printSuccess('ArrayModelCodec adds type semantics to array values')
// ============================================================================// Step 9: Nested ObjectModelCodec - Complex Structures// ============================================================================printStep(9, 'Nested ObjectModelCodec - Complex Structures')
printInfo('ObjectModelCodec can contain other ObjectModelCodecs for nested structures.')printInfo('')
// Define Address type (not the Algorand Address class)type PostalAddress = { street: string city: string postcode: number country?: string}
const postalAddressMetadata: ObjectModelMetadata<PostalAddress> = { name: 'PostalAddress', kind: 'object', fields: [ { name: 'street', wireKey: 'st', codec: stringCodec, optional: false }, { name: 'city', wireKey: 'ct', codec: stringCodec, optional: false }, { name: 'postcode', wireKey: 'pc', codec: numberCodec, optional: false }, { name: 'country', wireKey: 'co', codec: stringCodec, optional: true }, ],}
const postalAddressCodec = new ObjectModelCodec(postalAddressMetadata)
// Define Company type with nested addresstype Company = { name: string headquarters: PostalAddress founded?: number}
const companyMetadata: ObjectModelMetadata<Company> = { name: 'Company', kind: 'object', fields: [ { name: 'name', wireKey: 'n', codec: stringCodec, optional: false }, { name: 'headquarters', wireKey: 'hq', codec: postalAddressCodec, optional: false }, { name: 'founded', wireKey: 'f', codec: numberCodec, optional: true }, ],}
const companyCodec = new ObjectModelCodec(companyMetadata)
// Create a company with nested addressconst algorand: Company = { name: 'Algorand Foundation', headquarters: { street: '1 Innovation Drive', city: 'Boston', postcode: 12345, country: 'USA', }, founded: 2017,}
printInfo('Company type with nested PostalAddress:')printInfo(` ${JSON.stringify(algorand, null, 2).split('\n').map((l, i) => i === 0 ? l : ` ${ l}`).join('\n')}`)printInfo('')
const companyEncoded = companyCodec.encode(algorand, 'json')printInfo(`Encoded with nested wireKeys:`)printInfo(` ${JSON.stringify(companyEncoded)}`)printInfo(' Note: "headquarters" encoded as "hq", nested fields also renamed')printInfo('')
const companyDecoded = companyCodec.decode(companyEncoded, 'json')printInfo(`Decoded:`)printInfo(` name: "${companyDecoded.name}"`)printInfo(` headquarters.street: "${companyDecoded.headquarters.street}"`)printInfo(` headquarters.city: "${companyDecoded.headquarters.city}"`)printInfo(` founded: ${companyDecoded.founded}`)printInfo('')printSuccess('Nested ObjectModelCodecs preserve structure through encoding')
// ============================================================================// Step 10: Field Renaming with wireKey// ============================================================================printStep(10, 'Field Renaming with wireKey')
printInfo('wireKey allows mapping property names to different wire format keys.')printInfo('This is useful for:')printInfo(' - Reducing payload size (shorter keys)')printInfo(' - Matching external API specifications')printInfo(' - Maintaining backwards compatibility')printInfo('')
// Define type with verbose property namestype TransactionInfo = { transactionId: string senderAddress: string receiverAddress: string amountInMicroAlgos: bigint noteField?: string}
const transactionMetadata: ObjectModelMetadata<TransactionInfo> = { name: 'TransactionInfo', kind: 'object', fields: [ { name: 'transactionId', wireKey: 'txid', codec: stringCodec, optional: false }, { name: 'senderAddress', wireKey: 'snd', codec: stringCodec, optional: false }, { name: 'receiverAddress', wireKey: 'rcv', codec: stringCodec, optional: false }, { name: 'amountInMicroAlgos', wireKey: 'amt', codec: bigIntCodec, optional: false }, { name: 'noteField', wireKey: 'note', codec: stringCodec, optional: true }, ],}
const transactionCodec = new ObjectModelCodec(transactionMetadata)
const txn: TransactionInfo = { transactionId: 'ABC123...', senderAddress: 'SENDER...', receiverAddress: 'RECEIVER...', amountInMicroAlgos: 1000000n, noteField: 'Payment',}
printInfo('Property name to wireKey mapping:')printInfo(' transactionId -> txid')printInfo(' senderAddress -> snd')printInfo(' receiverAddress -> rcv')printInfo(' amountInMicroAlgos -> amt')printInfo(' noteField -> note')printInfo('')
printInfo(`Original: { transactionId: "${txn.transactionId}", senderAddress: "${txn.senderAddress}", ... }`)
const txnEncoded = transactionCodec.encode(txn, 'json')const txnEncodedObj = txnEncoded as Record<string, unknown>printInfo(`Encoded: { txid: "${txnEncodedObj.txid}", snd: "${txnEncodedObj.snd}", rcv: "${txnEncodedObj.rcv}", amt: "${txnEncodedObj.amt}", note: "${txnEncodedObj.note}" }`)printInfo('')
// Size comparisonconst withoutRenaming = `{"transactionId":"${txn.transactionId}","senderAddress":"${txn.senderAddress}","receiverAddress":"${txn.receiverAddress}","amountInMicroAlgos":"${txn.amountInMicroAlgos}","noteField":"${txn.noteField}"}`const withRenaming = `{"txid":"${txnEncodedObj.txid}","snd":"${txnEncodedObj.snd}","rcv":"${txnEncodedObj.rcv}","amt":"${txnEncodedObj.amt}","note":"${txnEncodedObj.note}"}`
printInfo(`Size comparison:`)printInfo(` Without wireKey renaming: ${withoutRenaming.length} bytes`)printInfo(` With wireKey renaming: ${withRenaming.length} bytes`)printInfo(` Savings: ${withoutRenaming.length - withRenaming.length} bytes (${Math.round((1 - withRenaming.length / withoutRenaming.length) * 100)}%)`)printInfo('')printSuccess('wireKey renaming reduces payload size and enables API matching')
// ============================================================================// Step 11: Round-Trip Verification// ============================================================================printStep(11, 'Round-Trip Verification')
printInfo('Verifying decode(encode(value)) === value for model codecs:')printInfo('')
const roundTrips: Array<{ name: string; format: EncodingFormat; success: boolean }> = []
// ObjectModelCodec - Personconst rtPersonDecoded = personCodec.decode(personCodec.encode(rtPerson, 'json'), 'json')roundTrips.push({ name: 'ObjectModelCodec<Person>', format: 'json', success: rtPerson.name === rtPersonDecoded.name && rtPerson.age === rtPersonDecoded.age && rtPerson.email === rtPersonDecoded.email,})
// ObjectModelCodec - AssetInfoconst rtAsset: AssetInfo = { assetId: 999n, name: 'Round Trip Asset', creator: new Address(new Uint8Array(32).fill(0xaa)), metadata: new Uint8Array([0xde, 0xad, 0xbe, 0xef]),}const rtAssetDecoded = assetCodec.decode(assetCodec.encode(rtAsset, 'msgpack'), 'msgpack')roundTrips.push({ name: 'ObjectModelCodec<AssetInfo>', format: 'msgpack', success: rtAsset.assetId === rtAssetDecoded.assetId && rtAsset.name === rtAssetDecoded.name && rtAsset.creator.equals(rtAssetDecoded.creator) && arrayEqual(rtAsset.metadata!, rtAssetDecoded.metadata!),})
// ObjectModelCodec - Nested Companyconst rtCompany: Company = { name: 'Test Corp', headquarters: { street: '123 Main', city: 'Anytown', postcode: 99999 }, founded: 2020,}const rtCompanyDecoded = companyCodec.decode(companyCodec.encode(rtCompany, 'json'), 'json')roundTrips.push({ name: 'ObjectModelCodec<Company> (nested)', format: 'json', success: rtCompany.name === rtCompanyDecoded.name && rtCompany.headquarters.street === rtCompanyDecoded.headquarters.street && rtCompany.headquarters.city === rtCompanyDecoded.headquarters.city && rtCompany.headquarters.postcode === rtCompanyDecoded.headquarters.postcode && rtCompany.founded === rtCompanyDecoded.founded,})
// PrimitiveModelCodec - Balanceconst rtBalance = 5000000nconst rtBalanceDecoded = balanceCodec.decode(balanceCodec.encode(rtBalance, 'json'), 'json')roundTrips.push({ name: 'PrimitiveModelCodec<bigint>', format: 'json', success: rtBalance === rtBalanceDecoded,})
// PrimitiveModelCodec - Stringconst rtName = 'Test Asset Name'const rtNameDecoded = assetNameCodec.decode(assetNameCodec.encode(rtName, 'msgpack'), 'msgpack')roundTrips.push({ name: 'PrimitiveModelCodec<string>', format: 'msgpack', success: rtName === rtNameDecoded,})
// ArrayModelCodec - Address[]const rtAddrList = [Address.zeroAddress(), new Address(new Uint8Array(32).fill(0xbb))]const rtAddrListDecoded = addressListCodec.decode(addressListCodec.encode(rtAddrList, 'json'), 'json')roundTrips.push({ name: 'ArrayModelCodec<Address[]>', format: 'json', success: rtAddrList.every((a, i) => a.equals(rtAddrListDecoded[i])),})
// ArrayModelCodec - number[]const rtScores = [100, 90, 80, 70]const rtScoresDecoded = scoresCodec.decode(scoresCodec.encode(rtScores, 'msgpack'), 'msgpack')roundTrips.push({ name: 'ArrayModelCodec<number[]>', format: 'msgpack', success: rtScores.every((s, i) => s === rtScoresDecoded[i]),})
// Display resultsfor (const rt of roundTrips) { const status = rt.success ? 'PASS' : 'FAIL' printInfo(` [${status}] ${rt.name} (${rt.format})`)}
const allPassed = roundTrips.every((rt) => rt.success)if (allPassed) { printInfo('') printSuccess('All round-trip verifications passed!')}
// ============================================================================// Step 12: Summary// ============================================================================printStep(12, 'Summary')
printInfo('Model codecs for structured object serialization:')printInfo('')printInfo(' ObjectModelCodec:')printInfo(' - new ObjectModelCodec(metadata)')printInfo(' - Encodes/decodes typed objects with field metadata')printInfo(' - Supports wireKey for field renaming')printInfo(' - Handles optional fields and nested objects')printInfo('')printInfo(' FieldMetadata:')printInfo(' - name: property name in TypeScript')printInfo(' - wireKey: key in wire format (optional, defaults to name)')printInfo(' - codec: how to encode/decode the field value')printInfo(' - optional: whether field can be omitted')printInfo('')printInfo(' PrimitiveModelCodec:')printInfo(' - new PrimitiveModelCodec(metadata)')printInfo(' - Wraps primitive codec with type semantics')printInfo(' - Adds named model for documentation')printInfo('')printInfo(' ArrayModelCodec:')printInfo(' - new ArrayModelCodec(metadata)')printInfo(' - Wraps ArrayCodec with type semantics')printInfo(' - Adds named model for documentation')printInfo('')printInfo(' Key Methods:')printInfo(' - encode(value, format) - always returns value')printInfo(' - encodeOptional(value, format) - returns undefined for defaults')printInfo(' - decode(value, format) - returns typed object')printInfo(' - decodeOptional(value, format) - preserves undefined')printInfo(' - defaultValue() - returns type default')printInfo('')printSuccess('Model Codecs Example completed!')