How to merge json feature flag configs with override support
- Step 1Paste the base flag config into JSON Input 1 — Input 1 holds your default flag state — the all-environments baseline (e.g. an export of default flag values). It must be a single valid JSON object. This is the layer everything else overrides.
- Step 2Paste the environment override into JSON Input 2 — Input 2 is the environment layer (
staging.jsonorproduction.json). Because inputs merge left-to-right with last-wins, anything here overrides the base for the same flag key. - Step 3Add a segment override with Add input — Click Add input to layer a user-segment or cohort override on top. Order is precedence: the last input wins, so put the most specific targeting layer last. Free tier allows 2 inputs; Pro allows up to 10.
- Step 4Select Deep merge — Keep the default Deep merge so nested flag objects (
rollout,targeting,variations) fold recursively. Use First wins if a layer should only add flags it introduces without ever flipping an existing one, or Array append if segment/variant arrays should accumulate across layers. - Step 5Click Merge Objects — Every input is parsed and folded in order. If a flag file isn't valid JSON the merge stops with the parser error — fix it and re-run. At least 2 non-empty inputs are required.
- Step 6Read the resolved state, then copy or download — Scan the output for the flags you expected to flip. Use Copy or Download (
merged.json). To prove an override only touched the flags you intended, diff the base against the merged output with the json-diff.
Strategy behaviour for flag configs
How each strategy resolves a collision on a flag key. 'Object' = both sides are flag objects (e.g. a flag with rollout/targeting); 'Array' = both are arrays (segment lists, variant arrays). Inputs fold left-to-right; 'later' means a more-specific layer.
| Strategy | Nested flag object | Segment / variant array | Boolean / number flag value | Typical use |
|---|---|---|---|---|
| Deep merge (default) | Recursively merged | Replaced by later array | Later wins | base → env → segment resolution |
| Shallow | Whole later object replaces earlier | Replaced by later array | Later wins | flat flag maps with no nested detail |
| First wins | Skipped if flag already defined | Skipped if already defined | Earlier kept | a layer that only introduces new flags |
| Array append | Whole later object replaces earlier | Concatenated | Later wins | accumulating segment/variant arrays |
Layered precedence example
How a single flag resolves across three layers under Deep merge. Last-defined value wins per leaf; absent leaves fall through to the base.
| Flag leaf | Base (Input 1) | Env: prod (Input 2) | Segment: beta (Input 3) | Resolved |
|---|---|---|---|---|
checkout.enabled | false | true | (absent) | true (from env) |
checkout.rollout.percent | 10 | (absent) | 100 | 100 (from segment) |
newNav.enabled | false | (absent) | (absent) | false (from base) |
betaBanner.enabled | (absent) | (absent) | true | true (from segment) |
Cookbook
Real base/override pairs from flag-resolution debugging, showing what each strategy produces. Flag names anonymised; all runs are client-side.
Environment flips a flag, keeps rollout detail
ExampleThe production layer turns checkout on but says nothing about rollout. Deep merge keeps the base's rollout object intact instead of dropping it — the whole reason to deep-merge flag configs.
Input 1 (base): { "checkout": { "enabled": false, "rollout": { "percent": 10 } } }
Input 2 (prod): { "checkout": { "enabled": true } }
Strategy: Deep merge
Output:
{
"checkout": {
"enabled": true,
"rollout": { "percent": 10 }
}
}Segment override bumps rollout to 100%
ExampleAdd a beta-segment layer that pushes the rollout to full. Three inputs fold left-to-right, so the segment (Input 3) wins the rollout leaf while env (Input 2) still owns enabled.
Input 1 (base): { "checkout": { "enabled": false, "rollout": { "percent": 10 } } }
Input 2 (prod): { "checkout": { "enabled": true } }
Input 3 (beta): { "checkout": { "rollout": { "percent": 100 } } }
Strategy: Deep merge
Output:
{
"checkout": {
"enabled": true,
"rollout": { "percent": 100 }
}
}Append targeted segment keys instead of replacing
ExampleA flag's segments array should accumulate across layers, not get overwritten. Deep merge replaces the array; Array append concatenates.
Input 1: { "promo": { "segments": ["us"] } }
Input 2: { "promo": { "segments": ["eu"] } }
Strategy: Deep merge → { "promo": { "segments": ["eu"] } }
Strategy: Array append → { "promo": { "segments": ["us", "eu"] } }
Note: array-append replaces nested OBJECTS last-wins;
only arrays are concatenated.First-wins: a layer that only adds new flags
ExampleAn experiment layer should introduce its own flags but never override one the base already pins. First wins keeps the base value for any shared key and only adds the new flag.
Input 1 (base): { "newNav": { "enabled": false } }
Input 2 (experiment): { "newNav": { "enabled": true }, "betaBanner": { "enabled": true } }
Strategy: First wins
Output:
{
"newNav": { "enabled": false },
"betaBanner": { "enabled": true }
}
→ newNav NOT flipped; betaBanner added.Resolve, then diff against the live snapshot
ExampleOnce you've folded the layers, compare the resolved JSON against what production is actually serving to spot drift. Merge here, then move both files to the diff tool.
Step 1 — merge base + prod + segment here → resolved.json Step 2 — paste resolved.json and live-export.json into json-diff json-diff output (illustrative): ~ checkout.rollout.percent: 100 → 50 - betaBanner (present resolved, missing live) → live is behind on two flags; redeploy.
Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Segment/variant arrays are replaced under deep merge
By designIf a flag's segments or variations is an array, the default deep strategy replaces it with the later layer's array rather than combining. A segment override with segments: ["eu"] will drop the base's ["us"]. Use Array append when arrays should accumulate, and always verify array-valued flag fields after merging.
A flag file isn't valid JSON
Parse errorFlag exports occasionally arrive with trailing commas or comments from a hand-edited config. JSON.parse rejects those and the merge stops with the parser message. Strip comments and trailing commas, or run the file through the json-format-fixer first.
Override sets a flag object to a bare boolean
Type mismatchIf the base has { "checkout": { "enabled": true, "rollout": {...} } } but an override has { "checkout": true }, the boolean replaces the whole object (object-vs-primitive is a type mismatch under last-wins). Keep your override shapes consistent with the base — override the leaf (checkout.enabled), not the whole flag, to preserve rollout/targeting.
Need more than 2 layers on free tier
Upgrade requiredBase + env + segment is three inputs, which exceeds the free tier's 2-input limit. Pro allows up to 10. Workaround on free: merge base + env, copy the result back into Input 1, then add the segment as Input 2 for a second pass.
Top-level is an array of flags, not a flag map
Index mergeSome platforms export flags as a JSON array of flag objects. The merger iterates object keys, so two arrays merge by index (flag at position 0 with flag at position 0) — not by flag key. Convert array exports to a key-by-id object first (e.g. with the json-transposer or a quick reshape), then merge.
Two layers define the same flag with the same value
No-opIf both an env and a segment override set checkout.enabled: true, the result is true either way — there's no conflict to surface. The merger won't flag redundant overrides; use the json-diff between layers if you want to find overrides that change nothing and could be deleted.
Flag set to null to 'unset' it
Last winsA layer with { "oldFlag": null } replaces whatever the base had (object or primitive) with null under last-wins — it does not delete the key. If your loader treats null as 'remove', strip nulls from the merged result afterward with the json-null-stripper.
Empty override input
IgnoredA blank textarea is skipped — it doesn't count toward the input total and contributes nothing. If every override is empty you'll fall below the 2-input minimum and the tool will ask for at least two non-empty inputs.
Frequently asked questions
Can this tool resolve LaunchDarkly / Unleash flag state?
It can fold exported flag JSON layers (base, environment, segment) with the same last-wins, deep-merge precedence those platforms use, so you can preview the effective state offline. It does not connect to any platform's API or evaluate full targeting rules — it's a static merge of the JSON you paste in. For genuine evaluation you still need the platform SDK; this is for debugging and previewing the merged config.
How is override precedence decided?
Inputs merge left-to-right and the last input wins. Put your base in Input 1, the environment override next, and the most specific (segment/cohort) override last. For nested flags, deep merge resolves each leaf independently, so a leaf absent from a later layer falls through to whichever earlier layer defined it.
Why did my rollout config disappear when I overrode enabled?
You probably used Shallow instead of Deep merge — Shallow replaces the whole flag object, so overriding enabled drops rollout. Re-run with Deep merge so the override folds into the existing flag object key-by-key and rollout survives.
Do segment arrays get combined across layers?
Not under the default deep strategy — arrays are replaced by the later layer. Select Array append if a flag's segments or variations array should accumulate across layers. Note that array-append only concatenates arrays; nested flag objects are still replaced last-wins in that mode.
How many flag layers can I merge?
Two on free, up to ten on Pro. A base + environment + segment merge is three inputs, so it needs Pro — or merge two at a time, copying the result back as the base for the next pass.
Are my flag names and targeting rules uploaded?
No. The merge runs entirely in your browser. Internal flag keys, segment names, and targeting detail never leave your tab. Only an anonymous run count is recorded for signed-in dashboard stats.
Can I model 'this layer only adds new flags'?
Yes — use First wins. It keeps every flag already defined in an earlier layer and only adds flags a later layer introduces, so an experiment or add-on layer can never accidentally flip a flag the base pins.
How do I find which flags an override actually changed?
Merge the layers here, copy the result, then drop the base config and the merged output into the json-diff. The diff lists exactly which flag leaves changed — handy for confirming an environment override touched only the flags you meant to touch.
What if a flag export is an array, not an object?
The merger merges objects by key; arrays merge by index, which is rarely what you want for flags. Reshape an array-of-flags export into a keyed object (id → flag) first, then merge. The json-transposer can help restructure flag exports.
Does it validate the flag schema?
No — it only merges. To check the resolved flag JSON is well-formed or matches a schema, use the json-validator. To derive a schema from a sample flag export, see the json-schema-generator.
Can I change output indentation?
The web tool always pretty-prints at 2 spaces with no indent control. Re-format the merged result with the json-prettifier for tabs or 4-space output, or the json-minifier to compact it for a fixture.
Can I automate the flag merge?
Yes — the merger runs as a runner-local tool on Pro, keeping internal flag config on your machine, with the same deep/first-wins/array-append strategies. A scheduled pipeline can fold base + env exports nightly and diff against the live snapshot to alert on drift.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.