jsonflattenelasticsearchgopython

JSON Flatten and Unflatten Explained — Elasticsearch, Logging, and Config

·8 min read

What is JSON Flattening?

JSON flattening converts a nested object into a flat key-value map where each key is a delimited path to the original value. Unflattening is the reverse — taking a flat map and reconstructing the nested structure.

Nested JSON:

json
{
  "user": {
    "name": "Ravi",
    "address": {
      "city": "Surat",
      "country": "IN"
    },
    "tags": ["admin", "developer"]
  },
  "active": true
}

Flattened (dot separator):

json
{
  "user.name": "Ravi",
  "user.address.city": "Surat",
  "user.address.country": "IN",
  "user.tags.0": "admin",
  "user.tags.1": "developer",
  "active": true
}

Arrays are indexed numerically: tags.0, tags.1, etc. The flat form and the nested form represent exactly the same data — just reorganized.

Why Flatten JSON?

Elasticsearch / OpenSearch: Nested JSON can cause "mapping explosion" — too many unique field names consuming memory. Pre-flattening to a controlled set of dot-notation keys keeps the index predictable and avoids dynamic mapping issues.

Logging pipelines (Datadog, Splunk, Loki, CloudWatch): Flat log events are easier to query, filter, and aggregate. Most logging agents parse flat key-value pairs better than deeply nested structures.

CSV export: Flat keys map directly to CSV column names. The JSONKit JSON to CSV converter uses flattening internally to handle nested objects.

MongoDB $set operator: MongoDB's $set uses dot notation to update specific fields without replacing the whole document:

javascript
db.users.updateOne({ _id: id }, {
  $set: { "address.city": "Ahmedabad" }  // dot notation — same as flattened key
});

Environment variables: Flat by nature. Tools like Viper (Go) and python-dotenv map env var names to config keys using . or __ separators — flattening bridges the gap.

Key-value stores (Redis, etcd, Vault): Store flat key → value pairs. Flattening a config object gives you the right shape for bulk writes.

JavaScript — Flatten and Unflatten

javascript
// Flatten a nested object
function flatten(obj, prefix = "", sep = ".") {
  const result = {};
  for (const [k, v] of Object.entries(obj)) {
    const key = prefix ? `${prefix}${sep}${k}` : k;
    if (v !== null && typeof v === "object" && !Array.isArray(v)) {
      Object.assign(result, flatten(v, key, sep));
    } else if (Array.isArray(v)) {
      v.forEach((item, i) => {
        const arrKey = `${key}${sep}${i}`;
        if (item !== null && typeof item === "object") {
          Object.assign(result, flatten(item, arrKey, sep));
        } else {
          result[arrKey] = item;
        }
      });
    } else {
      result[key] = v;
    }
  }
  return result;
}

// Unflatten a flat object back to nested
function unflatten(flat, sep = ".") {
  const result = {};
  for (const [key, value] of Object.entries(flat)) {
    const parts = key.split(sep);
    let current = result;
    for (let i = 0; i < parts.length - 1; i++) {
      if (!(parts[i] in current)) current[parts[i]] = {};
      current = current[parts[i]];
    }
    current[parts[parts.length - 1]] = value;
  }
  return result;
}

// Usage
const nested = { user: { name: "Ravi", address: { city: "Surat" } }, active: true };
const flat = flatten(nested);
// { "user.name": "Ravi", "user.address.city": "Surat", "active": true }

const restored = unflatten(flat);
// { user: { name: "Ravi", address: { city: "Surat" } }, active: true }

Using the flat npm package (battle-tested library):

javascript
// npm install flat
import { flatten, unflatten } from "flat";

const flat = flatten({ a: { b: { c: 1 } } });
// => { "a.b.c": 1 }

const restored = unflatten(flat);
// => { a: { b: { c: 1 } } }

// Custom separator
const underscored = flatten({ a: { b: 1 } }, { delimiter: "__" });
// => { "a__b": 1 }

Go Implementation

go
func flatten(obj map[string]interface{}, prefix string, sep string) map[string]interface{} {
    result := make(map[string]interface{})
    for k, v := range obj {
        key := k
        if prefix != "" {
            key = prefix + sep + k
        }
        switch val := v.(type) {
        case map[string]interface{}:
            for nk, nv := range flatten(val, key, sep) {
                result[nk] = nv
            }
        case []interface{}:
            for i, item := range val {
                idxKey := fmt.Sprintf("%s%s%d", key, sep, i)
                if nested, ok := item.(map[string]interface{}); ok {
                    for nk, nv := range flatten(nested, idxKey, sep) {
                        result[nk] = nv
                    }
                } else {
                    result[idxKey] = item
                }
            }
        default:
            result[key] = v
        }
    }
    return result
}

// Usage
nested := map[string]interface{}{
    "user": map[string]interface{}{
        "name":    "Ravi",
        "address": map[string]interface{}{"city": "Surat", "country": "IN"},
    },
    "active": true,
}
flat := flatten(nested, "", ".")
// flat["user.name"] = "Ravi"
// flat["user.address.city"] = "Surat"
// flat["user.address.country"] = "IN"
// flat["active"] = true

Python Implementation

python
def flatten(obj: dict, prefix: str = "", sep: str = ".") -> dict:
    result = {}
    for k, v in obj.items():
        key = f"{prefix}{sep}{k}" if prefix else k
        if isinstance(v, dict):
            result.update(flatten(v, key, sep))
        elif isinstance(v, list):
            for i, item in enumerate(v):
                arr_key = f"{key}{sep}{i}"
                if isinstance(item, dict):
                    result.update(flatten(item, arr_key, sep))
                else:
                    result[arr_key] = item
        else:
            result[key] = v
    return result

def unflatten(flat: dict, sep: str = ".") -> dict:
    result = {}
    for key, value in flat.items():
        parts = key.split(sep)
        d = result
        for part in parts[:-1]:
            d = d.setdefault(part, {})
        d[parts[-1]] = value
    return result

# Usage
nested = {"user": {"name": "Ravi", "address": {"city": "Surat"}}, "active": True}
flat = flatten(nested)
print(flat)
# {"user.name": "Ravi", "user.address.city": "Surat", "active": True}

restored = unflatten(flat)
# {"user": {"name": "Ravi", "address": {"city": "Surat"}}, "active": True}

Using the flatten-json package:

python
# pip install flatten-json
from flatten_json import flatten, unflatten_list

flat = flatten(nested_dict)         # default separator: _
flat_dot = flatten(nested_dict, ".") # custom separator

Separator Options

SeparatorUse caseExample
. (dot)Most JSON tools, Elasticsearch, MongoDBuser.address.city
__ (double underscore)Environment variables (AWS SSM, k8s secrets)USER__ADDRESS__CITY
/ (slash)Vault, etcd, some config formatsuser/address/city
[ bracket notationLodash _.set, some query languagesuser[address][city]

Use JSONKit's JSON Flatten tool for instant browser-based flattening and unflattening with configurable separators — no library needed.

Try JSON Flatten / Unflatten

Flatten nested JSON to dot-notation keys or unflatten back. Configurable separator.