jsonfeature-flagsdevopsjavascript

Feature Flags: The JSON Powering LaunchDarkly & Unleash

·8 min read·Developer Tools

Shipping code and releasing a feature are different events

Feature flags let a team deploy code continuously while controlling *who sees what feature and when* as a completely separate decision — no redeploy needed to turn something on, roll it back, or ramp it from 1% to 100% of users. Whether you use LaunchDarkly, Unleash, Flagsmith, or a homegrown system, the mechanism is the same: your app fetches a JSON payload describing every flag and its rules, evaluates it locally against the current user, and gets back a value.

A single flag's JSON shape

json
{
  "key": "new-checkout-flow",
  "enabled": true,
  "defaultVariant": "control",
  "variants": [
    { "key": "control", "value": false, "weight": 50 },
    { "key": "treatment", "value": true, "weight": 50 }
  ],
  "targets": [
    { "userId": "usr_9182", "variant": "treatment" }
  ]
}

enabled is the master kill switch — if false, the flag always evaluates to defaultVariant regardless of everything else. targets lets you force a specific user into a specific variant (useful for an internal team dogfooding a feature before wider rollout), overriding the percentage-based variants split for everyone else.

Percentage rollouts: deterministic, not random

A 50/50 rollout can't literally flip a coin on every request — the same user must consistently land in the same variant on every page load, or the UI would flicker between two experiences. The standard technique is consistent hashing: hash the user ID plus the flag key into a number, then map that number into a bucket:

javascript
function evaluateVariant(flag, userId) {
  if (!flag.enabled) return flag.defaultVariant;

  const override = flag.targets.find((t) => t.userId === userId);
  if (override) return override.variant;

  const hash = hashToPercent(flag.key + ":" + userId); // deterministic, 0-99
  let cumulative = 0;
  for (const variant of flag.variants) {
    cumulative += variant.weight;
    if (hash < cumulative) return variant.key;
  }
  return flag.defaultVariant;
}

The same userId and flag.key combination always hashes to the same bucket, so a user's assignment is stable across sessions — but a *different* flag on the same user hashes independently, so rollouts don't correlate with each other unless explicitly designed to.

Targeting rules: conditions beyond percentage

Real flag systems layer conditional rules before falling back to a percentage split — "100% on for internal staff, 10% for everyone else":

json
{
  "key": "new-checkout-flow",
  "rules": [
    { "condition": { "attribute": "email", "operator": "endsWith", "value": "@ourcompany.com" }, "variant": "treatment" },
    { "condition": { "attribute": "plan", "operator": "equals", "value": "enterprise" }, "variant": "control" }
  ],
  "variants": [
    { "key": "control", "value": false, "weight": 90 },
    { "key": "treatment", "value": true, "weight": 10 }
  ]
}

Rules are evaluated in order, top to bottom, with the first matching condition winning — the percentage variants split only applies to users who fall through *every* explicit rule unmatched, which is exactly why rule order matters and is usually significant in a flag platform's UI.

The client SDK payload: bulk-evaluated flags

Rather than one request per flag, most SDKs fetch every flag relevant to a user in one payload on app load, then evaluate all of them client-side without further network calls:

json
{
  "userId": "usr_9182",
  "flags": {
    "new-checkout-flow": { "value": true, "variant": "treatment" },
    "dark-mode-default": { "value": false, "variant": "control" },
    "beta-search": { "value": true, "variant": "treatment" }
  }
}

This is a pre-evaluated response — the server (or SDK, for local evaluation) has already run the targeting logic and just hands the client the final answer per flag, so the app doesn't need to know or care about rules and percentages at all; it just reads flags["new-checkout-flow"].value.

Local vs. server-side evaluation

Two architectures exist, and the JSON differs accordingly: server-side/remote evaluation sends the user's attributes to the flag service and gets back only the resolved values (as above); client-side/local evaluation downloads the *entire rule set* (including all the targeting logic and percentages from the earlier examples) once, then evaluates flags locally on every check with zero added latency — a common choice for high-frequency flag checks inside a hot code path, at the cost of exposing your targeting rules to anyone who inspects the downloaded payload.

Frequently asked questions

Rollout assignment uses a deterministic hash of the user ID and flag key, not randomness — this guarantees a stable, consistent experience across page loads and sessions rather than a flag flickering between values on every request.

Functionally similar in most systems, but enabled: false typically means "skip all evaluation logic, return the default" (cheap, fast), while a 0%-weighted variant still runs through the full targeting-rule evaluation before landing on a 0% bucket — semantically, "off" is the definitive kill switch.

Yes — the targets array (explicit per-user overrides) exists exactly for this: force your own account or QA test users into any variant regardless of the percentage split, before or during a gradual rollout.

Fetch the raw flag JSON and the user's attributes, then walk the rules array in order by hand — paste both into JSONKit's JSON Formatter to read the nested structure clearly, since the bug is almost always an earlier rule matching before you expected it to.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.