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.1.tar.gz (9.7 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.1-py3-none-any.whl (8.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: jsonshift-0.8.1.tar.gz
  • Upload date:
  • Size: 9.7 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.1.tar.gz
Algorithm Hash digest
SHA256 c67a0903b7d5877f7303d4ea45d62e72a647bd35ac93b46c7e8def0a615b02e1
MD5 d6437a44897391b1a61dd5b0246c17eb
BLAKE2b-256 6a282c1e44d6fd876e23ab3ffb961ebc524a269276bcfd361683f42e11ebcbf2

See more details on using hashes here.

File details

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

File metadata

  • Download URL: jsonshift-0.8.1-py3-none-any.whl
  • Upload date:
  • Size: 8.3 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8f586d4f5956feaac65f89d7a2ad7ae71cfea74841802b0a00b28775ffa5e225
MD5 8ecfc2f25eef2f5576ba9ede6b084d94
BLAKE2b-256 f0ce8cbb287c13ea3ca4a12facf31f1718a55245ab54453e2dc06987b1f00221

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