Built on top of the native json module with additional support for nested objects, variables, expressions, and post-load overrides via CLI.
Project description
JsonCastle
JsonCastle is a Python module built on top of the native json module for deserialization from JSON to data classes with additional support for:
- nested objects
- JSON variables
- environment variables
- arbitrary Python expressions
- post-load overrides, including overriding, adding, and removing collection elements via special CLI Overrides Syntax like
node.next_node.number=2,~node.tags[0],~node.tags[1:3],+node.next_node.tags=foo,~node.next_node.tags=buzz, etc.
With these features, you might find it especially useful in CI/CD pipelines.
Getting Started
Imagine you have a JSON configuration file for an automated process, but you want to run this process with slightly different variants of this file. Typically, you'd end up with multiple very similar configuration files, or you'd create an ad-hoc solution that would allow you to override certain parameters via CLI arguments. JsonCastle gives you such a solution out of the box for all properties of your data scheme, even for those in nested objects.
Since native json doesn't support variables, such configuration files might have a lot of repeated strings. Editing such files can be tedious and error-prone. You write a JSON file with variables and environment variables as the following example:
{
"$home_path": "%HOME%",
"$package_path": "${home_path}debug",
"exe_path": "${package_path}/mytool.exe",
"node": {
"number": 0,
"tags": ["foo", "bar"],
"next_node": {
"number": 1,
"tags": ["fizz", "buzz"]
}
},
"$pi": 3.14159,
"expressions": [
"1+1={{1+1}}",
"{{__import__('datetime').datetime.today().strftime('%B %d, %Y')}}",
"{{${pi}*2}}"
]
}
Assuming the HOME environment variable is set, for example, to C:/users/cicd/, then the value of the $package_path variable will be expanded from ${home_path}debug to C:/users/cicd/debug, and the value of exe_path will be C:/users/cicd/debug/mytool.exe.
In Python, you'd define your data classes that match the scheme:
from dataclasses import dataclass
@dataclass
class Cfg:
exe_path: str = None
node: Node = None
expressions: list[str] = None
@dataclass
class Node:
number: int
tags: list[str]
processed: False
next_node: Node = None
Now you can use the JsonCastle.load_from_file method to read and parse example.json and get an instance of the Cfg class:
from pprint import pprint
import sys
from json_castle import JsonCastle
cfg = JsonCastle.load_from_file(Cfg, "example.json", **JsonCastle.parse_args(sys.argv))
pprint(cfg)
If you run the Python script without any arguments, you’d see cfg printed as expected:
Cfg(exe_path='C:/users/cicd/debug/mytool.exe',
node={'next_node': {'number': 1, 'tags': ['fizz', 'buzz']},
'number': 0,
'tags': ['foo', 'bar']},
expressions=['1+1=2', 'March 07, 2026', '6.28318'])
However, by passing **JsonCastle.parse_args(sys.argv) as the third argument, you can override any values post-load via CLI. Running the script again with the arguments node.number=1 ~node.tags[0] node.next_node.number=2 +node.next_node.tags=foo ~node.next_node.tags=buzz will give you a cfg instance like this:
Cfg(exe_path='C:/users/cicd/debug/mytool.exe',
node={'next_node': {'number': 2, 'tags': ['fizz', 'foo']},
'number': 1,
'tags': ['bar']},
expressions=['1+1=2', 'March 07, 2026', '6.28318'])
If you want to deserialize your JSON from a string stream, you can use the static method load(cls, stream: IO[str], **kwargs), the usage is the same as of the load_from_file(cls, path: str, **kwargs) in the example above, but you provide your stream as the second argument instead of filepath.
Optionally, you can pass a second parameter to the JsonCastle.parse_args method to set the index from which arguments should be parsed, so you can reserve the preceding arguments for something else, i.e., you can use the first CLI argument to specify a path to a JSON file and start parsing overrides from the second one like this:
cfg = JsonCastle.load_from_file(Cfg, sys.argv[1], **JsonCastle.parse_args(sys.argv, 2))
CLI Overrides Syntax
Although some of the syntax for post-load overrides via the CLI was introduced already in the Getting Started section, here's an overview of all options with examples.
Basic Syntax
JsonCastle.parse_args method accepts a list of strings, where each element is a key=value pair except the one for removing an item by index, that only has a key, in which case, in the dictionary the method returns, the value is None.
Of course, you don't have to use this method and construct a dictionary by yourself instead; the JsonCastle.parse_args just provides you a convenient way for post-load overriding via CLI, which is particularly useful in CI/CD pipelines. Here are some examples of CLI args for overriding top-level pairs:
age=27 name=John full_name="John Smith" active=true balance=123.45
Nested Objects
If you need to override a nested value, you specify a path, separating objects with ., as you can see in the Getting Started. The following example shows how to override several pairs nested in the person object.
person.age=27 person.name=John person.full_name="John Smith" person.active=true person.balance=123.45
Working with Collections
When it comes to collections, you can change, add, or remove an item as well. If the collection is a nested object, the rules are the same as described in the Nested Objects section.
Overriding an Item
To override an item, you provide a path to the collection, in square brackets an index of the item you wish to override, and a new value on the right side of the = symbol. The following example shows how to change the value of the first element of the tags collection nested in the page object to programming.
page.tags[0]=programming
Adding a new Item
When you wish to add a new item to a collection, you prefix the key=value pair with a + symbol. The following example shows how to add a new value of python to the tags collection nested in the page object.
+page.tags=python
Removing an Item at Index
If you wish to remove an item at specific index, you prefix a path to the collection with index in square brackets of the element you wish to remove with the ~ symbol. The following example shows how to remove the second item from the tags collection nested in the page object.
~page.tags[1]
If any items follow the removed one, they will be pushed down, and the new length of the collection will be n-1.
Removing Items by Range
You can also remove multiple items by using a slice syntax. The following examples show how to remove all elements except the first one in the tags collection nested in the page object, how to remove all but the last two elements, and how to remove elements at index 1, 2, and 3, and all elements respectively.
~page.tags[1:]
~page.tags[:2]
~page.tags[1:3]
~page.tags[:]
Removing an Item by Value (Supports Regular Expressions)
If you wish to remove an item of a specific value, instead of the index as described in Removing an Item by Index, you provide a key=value pair prefixed with ~. The following example shows how to remove an item with a value of programming from the tags collection nested in the page object.
~page.tags=programming
If there is more than one occurrence of the programming string in the collection, only the first one will be removed. If you want to remove all occurrences, use ~~ instead of just one ~:
~~page.tags=programming
The value can be a regex. The following example shows how to remove all items with a value of a single word that starts with pro:
~~page.tags="\bpro\w*"
Removing Numerical Items by Condition
From numerical collections, you can remove all values that are less than (lt), less than or equal (lte), greater than (gt), or greater than or equal (gte) to a value on the right side of the argument. The following examples show how to remove values less than, less than or equal, greater than, and greater than or equal to 3 from the ratings collection, which itself is a property of the first supplier in the suppliers collection.
~suppliers[0].ratings=lt3
~suppliers[0].ratings=lte3
~suppliers[0].ratings=gt3
~suppliers[0].ratings=gte3
Additionally, you can join conditions together with the & operator. The following example shows how to remove all items from the ratings collection with values greater than 1.7 and less than or equal to 4.6 (i.e., [ 0, 1.7, 2.2, 3, 4.6, 5 ] -> [ 0, 1.7, 5 ]).
~ratings=gt1.7<e4.6
Flipping An Argument (Negation)
When removing items from a collection, you can also negate the effect by adding the ! symbol after ~ or ~~. The following example shows how to effectively keep just the first programming tag and remove all the others from the tags collection nested in the page object. Which is the exact opposite of what you can see in the first example above in Removing an Item by Value section, where the first occurrence of programming was removed and the rest of the items were kept.
~!page.tags=programming
This example shows how to keep all items that start with string pro (by regex):
~~!page.tags="\bpro\w*"
The following example shows how to remove from a numerical collection ratings all values less than or equal to 1.7 or greater than 4.6 (i.e., [ 0, 1.7, 2.2, 3, 4.6, 5 ] -> [ 2.2, 3, 4.6 ]):
~!ratings=gt1.7<e4.6
This example shows how to effectively keep just the first element of the suppliers collection:
~!suppliers[0]
And the last example shows how to remove all the elements except the first two:
~!suppliers[1:]
Python Expressions
Any string in JSON value between {{ and }} symbols will be treated as an arbitrary Python expression and replaced by the result of eval() function converted back to a string.
See the "expressions" collection at the bottom of the example JSON in Getting Started section.
Unit Tests
Unit Tests are not just a great way to ensure nothing is broken when a new feature is added, but also for implementing new features using TDD, which is the recommended way for extending this library. These tests can also serve as an overview of what the library is capable of. You can find all tests in the tests/test_json_castle.py file.
Future Ideas
- Add support for conditionals and loops in JSON.
Add mathematical expressions in JSON.✅- Extend the
parse_argsmethod to allow adding a custom object to a collection. Add regex support for removing items by value.✅When removing an item from a collection by value, let the user decide whether they want to remove just one or all items that match the value (✅~page.tags=programmingremoves the first;~!page.tags=programmingremoves all).Add support for removing items from a numerical collection by conditions✅>,<,<=, or=>.Add support for removing items by range (i.e.✅~items[1:4]would remove items at indices 1, 2, 3 and 4).- Add support for removing custom items by condition (i.e.,
~people={age < 16}would remove from the people collection all objects with the age key-value pair with age less than 16). Add support for datetime expressions in JSON.✅
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 json_castle-0.3.0.tar.gz.
File metadata
- Download URL: json_castle-0.3.0.tar.gz
- Upload date:
- Size: 15.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6d9b79a8647ec72f161d5c85cb1818d9021bd988a54d55f45bbcaa7f38f6584
|
|
| MD5 |
44260c77e3d57db45e394aa52b5447f6
|
|
| BLAKE2b-256 |
8b93fdd71509733f4693c830f6a4e26378c1f9fbe0a97c0a2545aa08dea65d92
|
Provenance
The following attestation bundles were made for json_castle-0.3.0.tar.gz:
Publisher:
publish-release.yml on marianpekar/json-castle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
json_castle-0.3.0.tar.gz -
Subject digest:
f6d9b79a8647ec72f161d5c85cb1818d9021bd988a54d55f45bbcaa7f38f6584 - Sigstore transparency entry: 1086500874
- Sigstore integration time:
-
Permalink:
marianpekar/json-castle@4f6bc652fe942623861e88ec6c823829dd725781 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/marianpekar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@4f6bc652fe942623861e88ec6c823829dd725781 -
Trigger Event:
release
-
Statement type:
File details
Details for the file json_castle-0.3.0-py3-none-any.whl.
File metadata
- Download URL: json_castle-0.3.0-py3-none-any.whl
- Upload date:
- Size: 9.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b55a3de151a0b78c5a837fc167f6b0e17b0ef4c67d01a41d9afb4134eae27df
|
|
| MD5 |
bac210fe41ae236c7eafffdfc96df9b6
|
|
| BLAKE2b-256 |
37dbfafb40ed6f01db4cef6c6dae71c09d7dafda893d391b5aca4f15a1ffb2b0
|
Provenance
The following attestation bundles were made for json_castle-0.3.0-py3-none-any.whl:
Publisher:
publish-release.yml on marianpekar/json-castle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
json_castle-0.3.0-py3-none-any.whl -
Subject digest:
7b55a3de151a0b78c5a837fc167f6b0e17b0ef4c67d01a41d9afb4134eae27df - Sigstore transparency entry: 1086500908
- Sigstore integration time:
-
Permalink:
marianpekar/json-castle@4f6bc652fe942623861e88ec6c823829dd725781 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/marianpekar
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-release.yml@4f6bc652fe942623861e88ec6c823829dd725781 -
Trigger Event:
release
-
Statement type: