json permutation library
Project description
J-Perm
A small, composable JSON-transformation DSL implemented in Python.
The library lets you describe transformations as data (a list of steps) and then apply them to an input document. It supports JSON Pointer paths, custom JMESPath expressions, interpolation with ${...} syntax, special reference/evaluation values, and a rich set of built-in operations.
J-Perm is built around a pluggable architecture: operations, special constructs, JMESPath functions, and casters are all registered independently and composed into an execution engine.
Features
-
JSON Pointer read/write with support for:
- root pointers (
"","/",".") - relative
..segments - list slices like
/items[1:3]
- root pointers (
-
Interpolation templates:
${/path/to/node}— JSON Pointer lookup${int:/path}/${float:/path}/${bool:/path}— type casters${? some.jmespath(expression) }— JMESPath with custom functions
-
Special values:
$ref— reference into the source document$eval— nested DSL evaluation with optional$select
-
Built-in operations:
set,copy,copyD,delete,assertforeach,if,distinctreplace_root,exec,update
-
Shorthand syntax for concise scripts (
~delete,~assert,field[], pointer assignments) -
Schema helper: approximate JSON Schema generation for a given DSL script
-
Fully extensible via registries:
- operations
- special constructs
- JMESPath functions
- casters
Architecture overview
J-Perm is composed from four independent registries:
| Registry | Purpose |
|---|---|
OpRegistry |
Registers DSL operations (op) |
SpecialRegistry |
Registers special values ($ref, $eval, …) |
JpFuncRegistry |
Registers custom JMESPath functions |
CasterRegistry |
Registers ${type:...} casters |
All registries can be imported directly from j_perm.
At runtime, these parts are wired together into an execution engine that evaluates the DSL.
Core API
ActionEngine.apply_actions
from j_perm import ActionEngine, Handlers, SpecialResolver, TemplateSubstitutor
Typical setup:
substitutor = TemplateSubstitutor()
special = SpecialResolver()
handlers = Handlers()
normalizer = Normalizer()
engine = ActionEngine(
handlers=handlers,
special=special,
substitutor=substitutor,
normalizer=normalizer,
)
Signature
apply_actions(
actions: Any,
*,
dest: MutableMapping[str, Any] | List[Any],
source: Mapping[str, Any] | List[Any],
) -> Mapping[str, Any]
actions— DSL script (list or mapping)dest— initial destination documentsource— source context available to pointers, interpolation,$ref,$eval- Returns a deep copy of the final
dest
Basic usage
source = {
"users": [
{"name": "Alice", "age": 17},
{"name": "Bob", "age": 22}
]
}
actions = [
# Start with empty list
{"op": "replace_root", "value": []},
# For each user - build a simplified object
{
"op": "foreach",
"in": "/users",
"as": "u",
"do": [
{
"op": "set",
"path": "/-",
"value": {
"name": "${/u/name}",
"is_adult": {
"$eval": [
{"op": "replace_root", "value": False},
{
"op": "if",
"cond": "${?`${/u/age}` >= `18`}",
"then": [{"op": "replace_root", "value": True}]
}
]
}
}
}
]
}
]
result = engine.apply_actions(actions, dest={}, source=source)
Interpolation & expression system (${...})
Interpolation is handled by TemplateSubstitutor and is used throughout operations such as set, copy, exec, update, schema building, etc.
JSON Pointer interpolation
${/path/to/value}
Resolves a JSON Pointer against the current source context.
Casters
${int:/age}
${float:/height}
${bool:/flag}
Casters are registered via CasterRegistry and can be extended by users.
JMESPath expressions
${? items[?price > `10`].name }
JMESPath expressions are evaluated against the source context with access to custom JMESPath functions registered in JpFuncRegistry.
Multiple templates
Any string may contain multiple ${...} expressions, resolved left-to-right.
Special values: $ref and $eval
Special values are resolved by SpecialResolver.
$ref
{ "$ref": "/path" }
Resolves a pointer against the source context and injects the value.
$eval
{ "$eval": [ ... ] }
Executes a nested DSL script using the same engine configuration and injects its result.
Shorthand syntax
Shorthand syntax is expanded during normalization:
~delete~assertfield[]- pointer assignments
These are converted into explicit operation steps before execution.
Built-ins
Built-in casters (${type:...})
Casters are registered in CasterRegistry and used in templates as ${name:<expr>}.
The <expr> part is first expanded (templates inside are allowed), then cast is applied.
int
Form: ${int:/path}
Behavior: int(value)
float
Form: ${float:/path}
Behavior: float(value)
str
Form: ${str:/path}
Behavior: str(value)
bool
Form: ${bool:/path}
Behavior: compatible with the old implementation:
- if value is
intorstr→bool(int(value)) - otherwise →
bool(value)
Examples:
${bool:1} -> True
${bool:"0"} -> False
${bool:"2"} -> True
${bool:""} -> False (falls back to bool("") == False)
Built-in special constructs ($ref, $eval)
Special values are resolved by SpecialResolver while walking value trees.
If a mapping contains a known special key, that handler takes over and the whole mapping is replaced by the resolved value.
$ref
Shape:
{ "$ref": "<pointer or template>", "$default": <optional> }
Behavior:
-
resolve
"$ref"through template substitution (so it may contain${...}) -
treat it as a pointer and read from source context
-
if pointer fails:
- if
"$default"exists → deep-copy and return default - else → re-raise (error)
- if
Example:
{ "op": "set", "path": "/user", "value": { "$ref": "/rawUser" } }
$eval
Shape:
{ "$eval": <actions>, "$select": "<optional pointer>" }
Behavior:
- run nested DSL using the same engine configuration
- if
"$select"is present → select a sub-value from the nested result
Example:
{
"op": "set",
"path": "/flag",
"value": {
"$eval": [
{ "op": "replace_root", "value": false },
{ "op": "replace_root", "value": true }
],
"$select": ""
}
}
Built-in JMESPath functions
Custom JMESPath functions are registered in JpFuncRegistry and available inside ${? ... }.
subtract(a, b)
Signature: subtract(number, number) -> number
Behavior: returns a - b
Example:
${? subtract(price, tax) }
Если у тебя есть другие встроенные JMES-функции в проекте (кроме
subtract), скажи их имена — я добавлю их описания в README в том же формате.
Built-in operations
All operations are registered in OpRegistry under their op name.
They are executed by ActionEngine.apply_actions() after normalization/shorthand expansion.
Common notes
-
Many operations accept values that may contain:
- special constructs (
$ref,$eval, and user-added ones) - templates (
${...})
- special constructs (
-
Unless stated otherwise: values are typically resolved as:
SpecialResolver.resolve(...)TemplateSubstitutor.substitute(...)- deep-copied before writing into
dest
set
Set or append a value at a JSON Pointer path in dest.
Shape:
{
"op": "set",
"path": "/pointer",
"value": <any>,
"create": true, // default: true
"extend": true // default: true
}
Semantics:
-
writes resolved
valueintodest[path] -
if
pathends with"/-"→ append to list -
if appending and
valueis a list:extend=true→ extend the listextend=false→ append list as one item
-
create=true→ create missing parent containers
copy
Copy value from source context to dest (internally uses set).
Shape:
{
"op": "copy",
"from": "/source/pointer",
"path": "/target/pointer",
"create": true, // default: true
"extend": true, // default: true
"ignore_missing": false, // default: false
"default": <any> // optional
}
Semantics:
-
"from"may be templated, then used as a pointer into source -
if missing:
ignore_missing=true→ no-op- else if
"default"provided → use default - else → error
copyD
Copy value from dest into another location in dest (self-copy).
Shape:
{
"op": "copyD",
"from": "/dest/pointer",
"path": "/target/pointer",
"create": true, // default: true
"ignore_missing": false, // default: false
"default": <any> // optional
}
Semantics:
"from"pointer is resolved against currentdest- pointer string itself may be templated with source context
delete
Delete a node at a pointer in dest.
Shape:
{
"op": "delete",
"path": "/pointer",
"ignore_missing": true // default: true
}
Notes:
- path must not end with
"-" - if missing and
ignore_missing=false→ error
assert
Assert node existence and optional equality in dest.
Shape:
{
"op": "assert",
"path": "/pointer",
"equals": <any> // optional
}
Semantics:
- if path missing →
AssertionError - if
equalsprovided and not equal →AssertionError
foreach
Iterate over an array (or mapping) from source context and execute nested actions.
Shape:
{
"op": "foreach",
"in": "/array/path",
"do": [ ... ],
"as": "item", // default: "item"
"default": [], // default: []
"skip_empty": true // default: true
}
Semantics:
-
resolve
"in"pointer against source context -
if missing → use
"default" -
if resolved value is a dict → iterate over items as pairs
-
for each element:
- extend source context with variable name
"as" - execute
"do"with same engine
- extend source context with variable name
-
on exception inside body → restore
destfrom snapshot
if
Conditionally execute nested actions.
Path-based mode:
{
"op": "if",
"path": "/pointer",
"equals": <any>, // optional
"exists": true, // optional
"then": [ ... ], // optional
"else": [ ... ], // optional
"do": [ ... ] // optional fallback success branch
}
Expression-based mode:
{
"op": "if",
"cond": "${?...}",
"then": [ ... ],
"else": [ ... ],
"do": [ ... ]
}
Semantics:
- one of
pathorcondmust be present thenruns on true,elseruns on falsedois used as “then” ifthenis missing- snapshot + restore on exceptions inside chosen branch
distinct
Remove duplicates from a list at dest[path], preserving order.
Shape:
{
"op": "distinct",
"path": "/list/path",
"key": "/key/pointer" // optional
}
Semantics:
- target must be a list
- if
keyprovided → key pointer is evaluated per item
replace_root
Replace the whole destination root with a new value.
Shape:
{
"op": "replace_root",
"value": <any>
}
Semantics:
- resolve specials/templates inside
value - deep-copy, then replace entire
dest
exec
Execute a nested script held inline or referenced from source context.
Pointer mode:
{
"op": "exec",
"from": "/script/path",
"default": <any>, // optional
"merge": false // default: false
}
Inline mode:
{
"op": "exec",
"actions": [ ... ],
"merge": false // default: false
}
Semantics:
-
exactly one of
from/actions -
if
fromcannot be resolved:- if
defaultpresent → use it (after specials/templates) - else → error
- if
-
merge=false:- run nested script with
dest={} - replace current
destwith result
- run nested script with
-
merge=true:- run nested script on current dest (like a sub-call)
update
Update a mapping at path using either source mapping (from) or inline mapping (value).
Shape:
{
"op": "update",
"path": "/target/path",
"from": "/source/path", // required in from-mode
"value": { ... }, // required in value-mode
"default": { ... }, // optional (from-mode only)
"create": true, // default: true
"deep": false // default: false
}
Semantics:
- exactly one of
from/value - update payload must be a mapping, else
TypeError - target at
pathmust be mutable mapping, elseTypeError deep=false→ shallowdict.updatedeep=true→ recursive merge for nested mappings
Shorthand normalization
In addition to explicit DSL steps, J-Perm supports a shorthand syntax for more concise scripts. Shorthand forms are expanded into regular operation steps before execution.
Shorthand expansion is implemented as a pluggable normalization layer, similar to operations and special constructs.
How it works
During normalization, each mapping entry is processed by a chain of registered shorthand rules:
- Each rule decides whether it can handle a given
(key, value)pair. - If a rule matches, it expands the entry into one or more explicit operation steps.
- The first matching rule wins.
- If no rule matches, normalization fails with an error.
The resulting list of steps is then executed by the engine as a normal DSL script.
Built-in shorthand rules
The following shorthand forms are enabled by default.
Delete shorthand (~delete)
{ "~delete": ["/a", "/b"] }
Expands into:
{ "op": "delete", "path": "/a" }
{ "op": "delete", "path": "/b" }
Assert shorthand (~assert)
Mapping form:
{ "~assert": { "/x": 10, "/y": 20 } }
Expands into:
{ "op": "assert", "path": "/x", "equals": 10 }
{ "op": "assert", "path": "/y", "equals": 20 }
List / string form:
{ "~assert": ["/x", "/y"] }
Expands into existence-only assertions.
Append shorthand (field[])
A key ending with [] means append to a list at that path:
{ "items[]": 123 }
Expands into:
{ "op": "set", "path": "/items/-", "value": 123 }
Pointer assignment shorthand
If a value is a string that starts with /, it is treated as a source pointer:
{ "name": "/user/fullName" }
Expands into:
{
"op": "copy",
"from": "/user/fullName",
"path": "/name",
"ignore_missing": true
}
Extending J-Perm
Custom operations
from ..op_handler import OpRegistry
@OpRegistry.register("my_op")
def my_op(step, dest, src, engine):
return dest
Custom special constructs
from j_perm import SpecialRegistry
@SpecialRegistry.register("$upper")
def sp_upper(node, src, resolver, engine):
value = resolver.substitutor.substitute(node["$upper"], src, engine)
return str(value).upper()
Custom JMESPath functions
from j_perm import JpFuncRegistry
from jmespath import functions as jp_funcs
@JpFuncRegistry.register("subtract")
@jp_funcs.signature({"types": ["number"]}, {"types": ["number"]})
def _subtract(self, a, b):
return a - b
Usage in DSL:
${? subtract(price, tax) }
Custom casters
from j_perm import CasterRegistry
@CasterRegistry.register("json")
def cast_json(x):
return json.loads(x)
Usage:
${json:/raw_payload}
Custom shorthand rules
from j_perm import ShorthandRegistry, ExpandResult
@ShorthandRegistry.register("name", priority=10)
def my_shorthand_rule(key: str, value: Any) -> ExpandResult | None:
if key.startswith("my_prefix_"):
# expand into steps
steps = [ ... ]
return steps
Plugin loading
Registries collect definitions at import time. To enable “use all registered components”, ensure that modules defining custom ops, specials, casters, or JMESPath functions are imported before engine construction.
A common pattern is to import all plugins in one place:
import my_project.jperm_plugins
License
This package is provided as-is; feel free to adapt it to your project structure.
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 j_perm-0.2.4.tar.gz.
File metadata
- Download URL: j_perm-0.2.4.tar.gz
- Upload date:
- Size: 26.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
109b42fc00e56d019798b09b402268cc766a1c6fc2680cf903c91bb9c3b83a02
|
|
| MD5 |
61ec611548c80ea3cd5179291285d05e
|
|
| BLAKE2b-256 |
a2d6811d98f2798f10bd741e6b5c51dabf7a36cb39192909a46b9424f0d5cc65
|
File details
Details for the file j_perm-0.2.4-py3-none-any.whl.
File metadata
- Download URL: j_perm-0.2.4-py3-none-any.whl
- Upload date:
- Size: 30.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
324a0f29b17b72faba594ac8cbd5d56211ce0bebb9d9fbb5b3b764e263df2be2
|
|
| MD5 |
812465221a523805286a0ac9d164ebaa
|
|
| BLAKE2b-256 |
aab2f1799b222a1fb9f9b8b91ffb4b03bec9f1ba801f597ceccd01d517e71dd9
|