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.

Engine rules (designed for system integrations):

  • If the source path does not exist → raises MappingMissingError.
  • If the source value is null / None → the destination receives None (not replaced).
  • defaults only fills values when the destination field is absent (does not overwrite None or existing values).
  • Supports dotted and indexed paths, e.g. a.b[0].c.
  • Extended mapper (ArrayMapper) supports wildcard list expansion using [*].
  • NEW: Support for optional mappings using optional: true.

🧩 Installation

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

🚀 Basic usage

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",          # None is preserved
    "contract.amount": "amount",
    "contract.installments": "installments"
  },
  "defaults": {
    "contract.type": "CCB",
    "contract.origin": "ORQ"
  }
}

out = Mapper().transform(spec, payload)
print(out)
# {
#   "customer": {"name": "John Doe", "cpf": null},
#   "contract": {"amount": 1000.0, "installments": 12, "type": "CCB", "origin": "ORQ"}
# }

🧠 ArrayMapper — mapping lists with [*]

The ArrayMapper extends Mapper and adds support for wildcard list mappings. You can transform lists of objects using the same declarative spec syntax.

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)
# {
#   "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"}
#   ]
# }

🧩 The same number of elements is preserved — each input list item becomes one transformed output item. Defaults with wildcards (new_products[*].currency) apply individually to every object in the list.


🆕 Optional fields (optional: true)

You can mark a mapping as optional, meaning:

  • If the source path exists → it is mapped normally
  • If the source path does NOT exist → it is silently ignored
  • No MappingMissingError is raised

This is useful when transforming payloads with optional fields.

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)
# {
#   "user": {
#     "full_name": "John Doe"
#   }
# }

Example (inside arrays)

from jsonshift 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)
# {
#   "items": [
#       {},
#       {"phone": "9999"}
#   ]
# }

Optional mappings never overwrite defaults, and defaults only apply when the destination field does not exist.


🖥️ Command-line interface (CLI)

# Transform using JSON files
jsonshift --spec spec.json --input payload.json

# or via stdin
cat payload.json | jsonshift --spec spec.json

▶️ Example run (from the repository)

This repository includes ready-to-use files under the examples/ folder.

# From the project root
jsonshift --spec examples/spec.json --input examples/payload.json

Expected output:

{
  "customer": {
    "name": "John Doe",
    "cpf": "12345678901"
  },
  "contract": {
    "amount": 1500.0,
    "installments": 6,
    "type": "CCB",
    "origin": "ORQ"
  }
}

📘 Spec format

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

⚠️ Error handling

  • MappingMissingError — source path not found (unless optional: true).
  • InvalidDestinationPath — invalid destination (e.g., destination with index).
  • TypeError — source list expected but got non-list in wildcard paths.

🧪 Testing

Run all unit tests:

pytest -v

Includes tests for:

  • Core Mapper
  • ArrayMapper wildcard behavior
  • Optional mappings (optional: true)

📄 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.8.0.tar.gz (9.4 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.8.0-py3-none-any.whl (8.0 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for jsonshift-0.8.0.tar.gz
Algorithm Hash digest
SHA256 295e2cda43f214a78fbc73b48386b1191f51b217562e7cfc151518a033b699d1
MD5 5561f722c44cd3f3318066a3d6cdc568
BLAKE2b-256 4d65e562c2526f095e3f72c17a5c2411c77c92f4ba9ccb330e71ecc2148aa1e8

See more details on using hashes here.

File details

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

File metadata

  • Download URL: jsonshift-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 8.0 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.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 df1c1ddd98ed4fd02b694721f388f8b6bc4d5f0e47d546c315369b21102aa0b3
MD5 7613e356948af4fb3472f0918c08cd8a
BLAKE2b-256 d1b23cf8f47890dd99c09ee80f8ae1b58f825560cbdca9ee1119d18348bc5b66

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