Skip to main content

A zero-dependency, deterministic JSON payload mapper that transforms source data into target structures using dotted paths and array indices.

Project description

✨ jsonshift

A lightweight Python package to convert one JSON payload into another using a declarative mapping spec defined in JSON.

Designed for deterministic system integrations, data pipelines, and API adapters.


⚙️ Engine rules

  • If the source path does not exist → raises MappingMissingError (unless optional: true is set)

  • If the source value is null / None → the destination receives None (defaults do NOT override None)

  • defaults only fill values when the destination field is absent (never overwrite existing values or None)

  • Supports dotted paths, indexed paths ([0], [1]) and wildcards ([*])

  • ArrayMapper supports:

    • wildcard list expansion ([*])
    • fixed index creation ([n])
    • automatic list creation
    • infinite nesting depth
  • NEW: Support for optional mappings using optional: true


🧩 Installation

pip install jsonshift
# or for development:
pip install -e .

🚀 Basic usage (Mapper)

from jsonshift import Mapper

payload = {
    "customer_name": "John Doe",
    "cpf": None,
    "amount": 1000.0,
    "installments": 12
}

spec = {
  "map": {
    "customer.name": "customer_name",
    "customer.cpf": "cpf",
    "contract.amount": "amount",
    "contract.installments": "installments"
  },
  "defaults": {
    "contract.type": "CCB",
    "contract.origin": "ORQ"
  }
}

out = Mapper().transform(spec, payload)
print(out)

Output:

{
  "customer": {
    "name": "John Doe",
    "cpf": null
  },
  "contract": {
    "amount": 1000.0,
    "installments": 12,
    "type": "CCB",
    "origin": "ORQ"
  }
}

🧠 ArrayMapper — mapping lists with [*]

ArrayMapper extends Mapper and adds powerful list handling.

from jsonshift.array_mapper import ArrayMapper

payload = {
    "products": [
        {"id": "P-001", "name": "Notebook", "price": 4500.0, "stock": 12},
        {"id": "P-002", "name": "Mouse Gamer", "price": 250.0, "stock": 100}
    ]
}

spec = {
    "map": {
        "new_products[*].code": "products[*].id",
        "new_products[*].title": "products[*].name",
        "new_products[*].price_brl": "products[*].price",
        "new_products[*].available": "products[*].stock"
    },
    "defaults": {
        "new_products[*].currency": "BRL"
    }
}

out = ArrayMapper().transform(spec, payload)
print(out)

Output:

{
  "new_products": [
    {
      "code": "P-001",
      "title": "Notebook",
      "price_brl": 4500.0,
      "available": 12,
      "currency": "BRL"
    },
    {
      "code": "P-002",
      "title": "Mouse Gamer",
      "price_brl": 250.0,
      "available": 100,
      "currency": "BRL"
    }
  ]
}

Each input list item maps to exactly one output item. Defaults with wildcards apply per element, never globally.


🧩 Fixed indices ([n]) and list creation

You can explicitly control list positions:

spec = {
    "map": {
        "items[0].name": "source.name_1",
        "items[1].name": "source.name_2"
    }
}

Lists are automatically created and expanded to fit the index.


🆕 Optional fields (optional: true)

Optional mappings allow graceful degradation when a source field is missing.

Rules

  • If the source exists → mapped normally

  • If the source does NOT exist:

    • no error is raised
    • destination structure is preserved
    • the final field is NOT created

Example — simple object

from jsonshift import Mapper

payload = {
    "name": "John Doe"
}

spec = {
    "map": {
        "user.full_name": "name",
        "user.nickname": {
            "path": "nickname",
            "optional": True
        }
    }
}

out = Mapper().transform(spec, payload)
print(out)

Output:

{
  "user": {
    "full_name": "John Doe"
  }
}

Example — optional fields inside arrays

from jsonshift.array_mapper import ArrayMapper

payload = {
    "users": [
        {"id": 1},
        {"id": 2, "phone": "9999"}
    ]
}

spec = {
    "map": {
        "items[*].phone": {
            "path": "users[*].phone",
            "optional": True
        }
    }
}

out = ArrayMapper().transform(spec, payload)
print(out)

Output:

{
  "items": [
    {},
    {"phone": "9999"}
  ]
}

🧬 Defaults with wildcards (advanced)

Defaults can create entire structures, even without map:

spec = {
    "defaults": {
        "a[*].b[*].c[*].value": 1
    }
}

Result:

{
  "a": [
    {
      "b": [
        {
          "c": [
            {"value": 1}
          ]
        }
      ]
    }
  ]
}

🖥️ Command-line interface (CLI)

jsonshift --spec spec.json --input payload.json
cat payload.json | jsonshift --spec spec.json

📘 Spec format

{
  "map": {
    "destination.path": "source.path",
    "destination[*].field": "source[*].field",
    "destination[0].field": "source.field"
  },
  "defaults": {
    "destination.path": "<fixed_value>",
    "destination[*].field": "<fixed_value>"
  }
}

⚠️ Error handling

  • MappingMissingError — source path not found (unless optional)
  • TypeError — wildcard source expected list but got non-list

🧪 Testing

pytest -v

Includes coverage for:

  • Core Mapper
  • ArrayMapper with wildcards and fixed indices
  • Defaults auto-creation
  • Optional mappings
  • Deep nesting and mixed scenarios

📄 License

MIT © 2025 Pedro Marques

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jsonshift-0.9.0.tar.gz (9.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jsonshift-0.9.0-py3-none-any.whl (8.4 kB view details)

Uploaded Python 3

File details

Details for the file jsonshift-0.9.0.tar.gz.

File metadata

  • Download URL: jsonshift-0.9.0.tar.gz
  • Upload date:
  • Size: 9.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for jsonshift-0.9.0.tar.gz
Algorithm Hash digest
SHA256 3370a673c9fa7444dfde25e116e33bf128a573e0189d002cb4474b0cab12764e
MD5 2300a2abc9ffd374e02fe7901e801c49
BLAKE2b-256 ae4a2560dbf537c1917465e7138b7f12caaff0b812cee7b1e22b2db09962c770

See more details on using hashes here.

File details

Details for the file jsonshift-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: jsonshift-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 8.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for jsonshift-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fe58dbb9b6d26e466e9e99402c148516d8edb16f49aa03607a41d84da6ff3a1f
MD5 8742104379081cfcf29fcfed533c908e
BLAKE2b-256 8eeb11f749f6a950b2e59239a9b602dc86addd1eaa1ec287276450e9f8d4dc49

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page