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(unlessoptional: trueis set) -
If the source value is
null/None→ the destination receivesNone(defaults do NOT overrideNone) -
defaultsonly fill values when the destination field is absent (never overwrite existing values orNone) -
Supports dotted paths, indexed paths (
[0],[1]) and wildcards ([*]) -
ArrayMappersupports:- wildcard list expansion (
[*]) - fixed index creation (
[n]) - automatic list creation
- infinite nesting depth
- wildcard list expansion (
-
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 ArrayMapperwith wildcards and fixed indices- Defaults auto-creation
- Optional mappings
- Deep nesting and mixed scenarios
📄 License
MIT © 2025 Pedro Marques
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3370a673c9fa7444dfde25e116e33bf128a573e0189d002cb4474b0cab12764e
|
|
| MD5 |
2300a2abc9ffd374e02fe7901e801c49
|
|
| BLAKE2b-256 |
ae4a2560dbf537c1917465e7138b7f12caaff0b812cee7b1e22b2db09962c770
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe58dbb9b6d26e466e9e99402c148516d8edb16f49aa03607a41d84da6ff3a1f
|
|
| MD5 |
8742104379081cfcf29fcfed533c908e
|
|
| BLAKE2b-256 |
8eeb11f749f6a950b2e59239a9b602dc86addd1eaa1ec287276450e9f8d4dc49
|