Schema Versioning & Remapping
CFGPack supports firmware upgrades where the configuration schema changes between versions. This is handled through:
Schema name embedding: The schema name is automatically stored at reserved index 0 in serialized blobs
Version detection: Read the schema name from a blob to determine which schema version created it
Index remapping: Map old indices to new indices when loading config from an older schema version
Type widening: Automatically coerce values to wider types (e.g., u8 → u16) during remapping
Reserved Index
Index 0 (CFGPACK_INDEX_RESERVED_NAME) is reserved for the schema name. User-defined schema entries should use indices starting at 1.
Detecting Schema Version
Use cfgpack_peek_name() to read the schema name from a serialized blob without fully loading it:
uint8_t blob[4096];
size_t blob_len;
// ... load blob from storage ...
char name[64];
cfgpack_err_t err = cfgpack_peek_name(blob, blob_len, name, sizeof(name));
if (err == CFGPACK_OK) {
printf("Config was created with schema: %s\n", name);
} else if (err == CFGPACK_ERR_MISSING) {
printf("No schema name in blob (legacy format)\n");
}
Migrating Between Schema Versions
When loading config from an older schema version, use cfgpack_pagein_remap() with a remap table:
// Old schema "sensor_v1" had:
// index 1: temp (u8)
// index 2: humid (u8)
//
// New schema "sensor_v2" has:
// index 1: temp (u16) -- widened type, same index
// index 5: humid (u16) -- moved to new index, widened type
// index 6: press (u16) -- new field
// Define remap table: old_index -> new_index
cfgpack_remap_entry_t remap[] = {
{1, 1}, // temp: index unchanged (type widening handled automatically)
{2, 5}, // humid: moved from index 2 to index 5
};
// Load with remapping
cfgpack_err_t err = cfgpack_pagein_remap(&ctx, blob, blob_len, remap, 2);
if (err == CFGPACK_OK) {
// Old values loaded into new schema positions
// New fields (like press at index 6) retain their defaults
}
Default Restoration During Remap
After cfgpack_pagein_remap() decodes all entries from the old data, it automatically restores presence for any new-schema entries that have has_default set but were not covered by the incoming data. This means:
New entries added in a schema upgrade that have default values are immediately accessible via
cfgpack_get()after migration, without any explicit code to set them.Entries without defaults (
NIL) that were not in the old data remain absent (cfgpack_get()returnsCFGPACK_ERR_MISSING).If old data contains a value for an entry, that decoded value always takes precedence over the schema default.
This applies to all types including str and fstr defaults.
Type Widening Rules
During remapping, values can be automatically widened to larger types:
From |
To (allowed) |
|---|---|
u8 |
u16, u32, u64 |
u16 |
u32, u64 |
u32 |
u64 |
i8 |
i16, i32, i64 |
i16 |
i32, i64 |
i32 |
i64 |
f32 |
f64 |
fstr |
str (if length fits) |
Narrowing conversions (e.g., u16 → u8) return CFGPACK_ERR_TYPE_MISMATCH.
Migration Workflow
A typical firmware upgrade migration:
// 1. Read schema name from stored config
char stored_name[64];
cfgpack_err_t err = cfgpack_peek_name(flash_data, flash_len, stored_name, sizeof(stored_name));
// 2. Compare with current schema
if (strcmp(stored_name, current_schema.map_name) == 0) {
// Same schema version - load directly
cfgpack_pagein_buf(&ctx, flash_data, flash_len);
} else if (strcmp(stored_name, "myapp_v1") == 0) {
// Old v1 schema - apply v1->v2 remap
cfgpack_pagein_remap(&ctx, flash_data, flash_len, v1_to_v2_remap, remap_count);
} else {
// Unknown schema - use defaults
printf("Unknown config version, using defaults\n");
}
Working Examples
See examples/low_memory/ for a complete v1 -> v2 migration using JSON schemas with cfgpack_schema_measure(), examples/fleet_gateway/ for a three-version migration chain (v1 -> v2 -> v3) using msgpack binary schemas with cfgpack_schema_measure_msgpack(), or examples/flash_config/ for a LittleFS-backed migration with LZ4-compressed msgpack schemas and all five migration scenarios (keep, widen, move, remove, add).