Persistence & Storage Format
This page documents the JSON formats used to persist and transport Konditional configuration:
- Snapshot: the full configuration payload (
ConfigurationSnapshotCodec.encode(...)) - Patch: an incremental update payload (
ConfigurationSnapshotCodec.applyPatchJson(...))
These formats are intentionally storage-agnostic: you can store the JSON in files, a database, or ship it over the network.
Mental model: what is persisted
At persistence time, the library serializes a list of flag definitions:
FeatureId format (key)
Each flag is stored under a stable FeatureId string:
feature::${namespaceIdentifierSeed}::${featureKey}
Where:
${namespaceIdentifierSeed}isNamespace.identifierSeed(defaults to the namespaceid)${featureKey}is the feature key (typically the Kotlin property name)
Example (Global namespace, darkMode):
feature::global::darkMode
Backward compatibility: older snapshots may contain value::${namespaceIdentifierSeed}::${featureKey}. These are
normalized on load.
Value encoding (defaultValue / rule value)
Both defaultValue and each rule's value are encoded as a discriminated union (tagged object). The type field
acts as a discriminator that determines
which variant is being used:
{
"type": "BOOLEAN",
"value": true
}
Supported shapes:
| Kind | JSON shape |
|---|---|
| Boolean | { "type": "BOOLEAN", "value": ${boolean} } |
| String | { "type": "STRING", "value": "${string}" } |
| Int | { "type": "INT", "value": ${int} } |
| Double | { "type": "DOUBLE", "value": ${double} } |
| Enum | { "type": "ENUM", "value": "${enumName}", "enumClassName": "${fqcn}" } |
| Data class | { "type": "DATA_CLASS", "dataClassName": "${fqcn}", "value": { ...primitive fields... } } |
The DATA_CLASS representation stores a primitive map of fields along with the fully qualified class name.
The generated OpenAPI schema includes a discriminator block that maps each type value to its corresponding schema
reference (e.g., "BOOLEAN" →
#/components/schemas/BooleanFlagValue).
Version range encoding (versionRange)
Rules may include a versionRange object. Like flag values, it uses a discriminated union pattern where the type
field determines which variant and
required fields apply:
{
"type": "MIN_AND_MAX_BOUND",
"min": {
"major": 2,
"minor": 0,
"patch": 0
},
"max": {
"major": 4,
"minor": 0,
"patch": 0
}
}
Valid type values:
UNBOUNDEDMIN_BOUND(requiresmin)MAX_BOUND(requiresmax)MIN_AND_MAX_BOUND(requiresminandmax)
Locale and platform values are serialized as their stable ids (LocaleTag.id / PlatformTag.id). The built-in
AppLocale and Platform enums use their enum names as ids, so existing snapshots remain stable.
Ramp-up allowlists (rampUpAllowlist)
Both flags and individual rules may include a rampUpAllowlist field:
- It is a set of stable ID hex strings (the same representation returned by
Context.stableId.id). - It does not force a rule to match; it only bypasses the ramp-up check after a rule matches by criteria.
- Flag-level and rule-level allowlists are treated as a union (either can bypass ramp-up).
- It does not override
isActiveor the namespace kill-switch (disableAll).
To generate a value for remote config, use the same normalization as the runtime:
val stableIdHex = StableId.of("user-123").id // "757365722d313233"
val stableIdFromHex = StableId.fromHex(stableIdHex)
Generic templates (Kotlin-style)
Snapshot template
val snapshotJson = """
{
"meta": {
"version": "${optionalVersionOrNull}",
"generatedAtEpochMillis": ${optionalEpochMillisOrNull},
"source": "${optionalSourceOrNull}"
},
"flags": [
{
"key": "feature::${namespaceSeed}::${featureKey}",
"defaultValue": {
"type": "${valueType}",
"value": ${defaultValueJson},
"...": "${typeSpecificFields}"
},
"salt": "${salt}",
"isActive": ${isActive},
"rampUpAllowlist": ["${stableIdHex}", "..."],
"rules": [
{
"value": {
"type": "${valueType}",
"value": ${ruleValueJson},
"...": "${typeSpecificFields}"
},
"rampUp": ${rampUpPercent},
"rampUpAllowlist": ["${stableIdHex}", "..."],
"note": "${optionalNoteOrNull}",
"locales": ["${APP_LOCALE_ENUM_NAME}", "..."],
"platforms": ["${PLATFORM_ENUM_NAME}", "..."],
"axes": {
"${axisId}": ["${axisValueId}", "..."]
},
"versionRange": {
"type": "${UNBOUNDED|MIN_BOUND|MAX_BOUND|MIN_AND_MAX_BOUND}",
"min": { "major": ${minMajor}, "minor": ${minMinor}, "patch": ${minPatch} },
"max": { "major": ${maxMajor}, "minor": ${maxMinor}, "patch": ${maxPatch} }
}
}
]
}
]
}
"""
Patch template
val patchJson = """
{
"meta": {
"version": "${optionalVersionOrNull}",
"generatedAtEpochMillis": ${optionalEpochMillisOrNull},
"source": "${optionalSourceOrNull}"
},
"flags": [
{ "...": "flag objects (same shape as snapshot)" }
],
"removeKeys": [
"feature::${namespaceSeed}::${featureKeyToRemove}",
"feature::${namespaceSeed}::${anotherKey}"
]
}
"""
Parse boundary and failure modes
Deserialization reconstructs configurations by looking up each key in an internal registry of known features.
- If a key is not registered, parsing fails with
ParseError.FeatureNotFound. - JSON syntax/shape errors fail with
ParseError.InvalidJson/ParseError.InvalidSnapshot.
Operationally: treat parse failures as “reject update, keep last-known-good”.
Snapshot metadata (meta)
Snapshots and patches may include an optional meta object:
{
"meta": {
"version": "rev-123",
"generatedAtEpochMillis": 1700000000000,
"source": "s3://configs/global.json"
},
"flags": []
}
JSON examples
Notes:
- Incoming JSON may omit fields that have defaults (for example
rampUpAllowlist,locales,platforms,axes, andversionRange). ConfigurationSnapshotCodec.encode(...)emits explicit values for these fields (including empty arrays/objects).
:::details Snapshot: booleans + string variants, with version ranges
{
"flags": [
{
"key": "feature::global::darkMode",
"defaultValue": {
"type": "BOOLEAN",
"value": false
},
"salt": "v1",
"isActive": true,
"rampUpAllowlist": [],
"rules": [
{
"value": {
"type": "BOOLEAN",
"value": true
},
"rampUp": 50.0,
"rampUpAllowlist": [
"757365722d313233"
],
"note": "iOS gradual ramp-up",
"locales": [
"UNITED_STATES"
],
"platforms": [
"IOS"
],
"axes": {},
"versionRange": {
"type": "MIN_BOUND",
"min": {
"major": 2,
"minor": 0,
"patch": 0
}
}
}
]
},
{
"key": "feature::global::apiEndpoint",
"defaultValue": {
"type": "STRING",
"value": "https://api.example.com"
},
"salt": "v1",
"isActive": true,
"rampUpAllowlist": [],
"rules": [
{
"value": {
"type": "STRING",
"value": "https://api-ios.example.com"
},
"rampUp": 100.0,
"rampUpAllowlist": [],
"note": "iOS endpoint",
"locales": [],
"platforms": [
"IOS"
],
"axes": {},
"versionRange": {
"type": "UNBOUNDED"
}
},
{
"value": {
"type": "STRING",
"value": "https://api-android.example.com"
},
"rampUp": 100.0,
"rampUpAllowlist": [],
"note": "Android endpoint",
"locales": [],
"platforms": [
"ANDROID"
],
"axes": {},
"versionRange": {
"type": "UNBOUNDED"
}
}
]
}
]
}
:::
:::details Snapshot: enum value payload
{
"flags": [
{
"key": "feature::global::theme",
"defaultValue": {
"type": "ENUM",
"value": "LIGHT",
"enumClassName": "com.example.Theme"
},
"salt": "v1",
"isActive": true,
"rampUpAllowlist": [],
"rules": [
{
"value": {
"type": "ENUM",
"value": "DARK",
"enumClassName": "com.example.Theme"
},
"rampUp": 100.0,
"rampUpAllowlist": [],
"note": "Dark theme for iOS",
"locales": [],
"platforms": [
"IOS"
],
"axes": {},
"versionRange": {
"type": "UNBOUNDED"
}
}
]
}
]
}
:::
:::details Patch: update one flag, remove one flag
{
"flags": [
{
"key": "feature::global::darkMode",
"defaultValue": {
"type": "BOOLEAN",
"value": false
},
"salt": "v1",
"isActive": true,
"rampUpAllowlist": [],
"rules": [
{
"value": {
"type": "BOOLEAN",
"value": true
},
"rampUp": 100.0,
"rampUpAllowlist": [],
"note": "Ramp-up complete",
"locales": [],
"platforms": [],
"axes": {},
"versionRange": {
"type": "UNBOUNDED"
}
}
]
}
],
"removeKeys": [
"feature::global::LEGACY_SUPPORT"
]
}
:::
:::details Consumer Configuration Lifecycle Sample
{
"flags": [
{
"key": "feature::consumer-lifecycle-3f02840e-abd0-4447-9bdc-0a8f41ca530e::darkMode",
"defaultValue": {
"type": "BOOLEAN",
"value": false
},
"salt": "v1",
"isActive": true,
"rules": [
{
"value": {
"type": "BOOLEAN",
"value": true
},
"rampUp": 100.0,
"note": "iOS fully enabled",
"locales": [],
"platforms": [
"IOS"
],
"versionRange": {
"type": "UNBOUNDED"
}
}
]
},
{
"key": "feature::consumer-lifecycle-3f02840e-abd0-4447-9bdc-0a8f41ca530e::apiEndpoint",
"defaultValue": {
"type": "STRING",
"value": "https://api.example.com"
},
"salt": "v1",
"isActive": true,
"rules": [
{
"value": {
"type": "STRING",
"value": "https://api-web.example.com"
},
"rampUp": 100.0,
"note": "Web endpoint override",
"locales": [],
"platforms": [
"WEB"
],
"versionRange": {
"type": "UNBOUNDED"
}
}
]
},
{
"key": "feature::consumer-lifecycle-3f02840e-abd0-4447-9bdc-0a8f41ca530e::maxRetries",
"defaultValue": {
"type": "INT",
"value": 3
},
"salt": "v1",
"isActive": true,
"rules": [
{
"value": {
"type": "INT",
"value": 5
},
"rampUp": 100.0,
"note": "More retries on v2+",
"locales": [],
"platforms": [],
"versionRange": {
"type": "MIN_BOUND",
"min": {
"major": 2,
"minor": 0,
"patch": 0
}
}
}
]
},
{
"key": "feature::consumer-lifecycle-3f02840e-abd0-4447-9bdc-0a8f41ca530e::theme",
"defaultValue": {
"type": "ENUM",
"value": "LIGHT",
"enumClassName": "io.amichne.konditional.serialization.ConsumerConfigurationLifecycleTest$Theme"
},
"salt": "v1",
"isActive": true,
"rules": [
{
"value": {
"type": "ENUM",
"value": "DARK",
"enumClassName": "io.amichne.konditional.serialization.ConsumerConfigurationLifecycleTest$Theme"
},
"rampUp": 100.0,
"note": "Dark theme for FR locale",
"locales": [
"FRANCE"
],
"platforms": [],
"versionRange": {
"type": "UNBOUNDED"
}
}
]
},
{
"key": "feature::consumer-lifecycle-3f02840e-abd0-4447-9bdc-0a8f41ca530e::userSettings",
"defaultValue": {
"type": "DATA_CLASS",
"dataClassName": "io.amichne.konditional.serialization.ConsumerConfigurationLifecycleTest$UserSettings",
"value": {
"enabled": true,
"maxRetries": 3.0,
"theme": "light",
"timeoutSeconds": 30.0
}
},
"salt": "v1",
"isActive": true,
"rules": [
{
"value": {
"type": "DATA_CLASS",
"dataClassName": "io.amichne.konditional.serialization.ConsumerConfigurationLifecycleTest$UserSettings",
"value": {
"enabled": false,
"maxRetries": 5.0,
"theme": "dark",
"timeoutSeconds": 10.0
}
},
"rampUp": 100.0,
"note": "Custom settings for iOS",
"locales": [],
"platforms": [
"IOS"
],
"versionRange": {
"type": "UNBOUNDED"
}
}
]
}
]
}
:::