Skip to content

Latest commit

 

History

History
397 lines (298 loc) · 12.2 KB

File metadata and controls

397 lines (298 loc) · 12.2 KB

csonpath

That's not my path, that's not your path, but csonpath.

Project Sandbox

csonpath is a partial JSONPath implementation in C, with Python bindings. It allows you to query, update, and remove data from JSON objects using path expressions.

Unlike many JSONPath libraries, csonpath is backend-agnostic: it can work with any C library or environment that manipulates array, object, and scalar types—not just JSON. Out of the box, backends for json-c, yyjson, Python and Rust (via serde_json) are provided.


🚀 Features

JSONPath Syntax

Feature Example Description
Dot notation $.a.b Access nested object fields
Bracket notation $['a']['b'] Alternative object/array access
Array index $.array[0] Access by zero-based index
Wildcard [*] $.array[*].field Iterate all array elements
Recursive descent .. $..name Search recursively for a key
Union , (inside brackets) $['a','b'], $.array[0,1], $.items[?n==1, ?n==2] Match all listed selectors at once
OR fallback | $.a | $.b Try the left path first; fall back to the right one if it does not match. Only the first successful path is used.
Filters $.items[?price > 10] Filter array elements
Regex filters $.items[?name =~ "foo"] POSIX regex-based filtering
Multiple filters (&) $.items[?a=1 & b=2] Combine conditions
Subpath expressions $.obj[$.key] Use JSON values as dynamic path keys
@ current object $.items[?@.price > 10] Reference the current element in filters

Operations

  • Find First — retrieve the first match.
  • Find All — retrieve all matches (returns an array).
  • Update or Create — modify existing values or create missing ones.
  • Remove — delete matching elements.
  • Callback — execute a custom callback on each match.
  • Update or Create Callback — traverse the path, creating missing intermediate objects and arrays, then invoke the callback on each leaf node.

🌐 Links


📄 Table of Contents


📦 Installation

Prerequisites

  • C compiler (gcc or clang)
  • json-c library (for C usage)
  • Python 3.x (for Python bindings)

C (json-c)

Just include the header in your project. There is no separate install step required:

#include "csonpath_json-c.h"

Make sure to link against json-c when compiling:

gcc myapp.c -o myapp $(pkg-config --cflags --libs json-c)

C (Rust backend)

The Rust backend is located in rust/. It builds both a safe Rust API and the C glue required by the csonpath core.

cd rust
cargo build
cargo test

To use it from C, include the backend header and link against the produced static library:

#include "rust/csonpath_rust_backend.h"

Python

Install from PyPI:

pip install csonpath

To install from source (development):

pip install .
# or
make pip-dev

🛠️ Usage

C (json-c)

#include "csonpath_json-c.h"

static void my_cb(json_object *parent, struct csonpath_child_info *info,
                  json_object *current, void *ud)
{
    json_object_set_string(current, "modified");
}

int main(void)
{
    struct json_object *jobj = json_tokener_parse(json_str);
    struct csonpath *p = csonpath_new("$.a");

    /* Find First: return the first match, or NULL */
    struct json_object *ret = csonpath_find_first(p, jobj);

    /* Find All: return a NEW json_object array. Caller must free it. */
    ret = csonpath_find_all(p, jobj);
    json_object_put(ret);

    /* Remove: delete matching keys (or set array slots to null). Returns count. */
    int removed = csonpath_remove(p, jobj);

    /* Update or Create: replace matches, or create the full path if missing. */
    csonpath_update_or_create(p, jobj, json_object_new_string("new_value"));

    /* Callback: call a user function for every match. */
    csonpath_callback(p, jobj, my_cb, NULL);

    /* Update or Create Callback: like callback, but creates missing parents first,
       then invokes the callback on every leaf (existing or newly created). */
    csonpath_update_or_create_callback(p, jobj, my_cb, NULL);

    csonpath_destroy(p);
    json_object_put(jobj);
    return 0;
}

Python

import csonpath

data = {"a": "value", "array": [1, 2, 3]}
p = csonpath.CsonPath("$.a")

# Find First / Find All
p.find_first(data)   # -> "value"
p.find_all(data)     # -> ["value"]

# Remove: returns number of removed items
p.set_path("$.array[*]")
p.remove(data)

# Update or Create: builds missing objects/arrays automatically
p.set_path("$.x.y.z")
p.update_or_create(data, [])
# data is now {"a": "value", "array": [1, 2, 3], "x": {"y": {"z": []}}}

# Callback
p.set_path("$.a")
p.callback(data, lambda parent, idx, cur, _: parent.__setitem__(idx, cur.upper()))

# Update or Create Callback: creates parents, then calls cb on each leaf
p.set_path("$[*].a")
p.update_or_create_callback(dst, my_sync_fn, userdata)

📘 C API Reference

struct csonpath *csonpath_new(const char *path);

Create and initialize a new csonpath object.

int csonpath_set_path(struct csonpath *p, const char *path);

Change the path of an existing object.

int csonpath_compile(struct csonpath *p);

Compile the path expression. This is optional—paths are compiled automatically on first use—but explicit compilation can help catch syntax errors earlier.

void csonpath_print_instruction(struct csonpath *p);

Print the compiled bytecode instructions (useful for debugging).

struct json_object *csonpath_find_first(struct csonpath *p, struct json_object *json);

Return the first matching value, or NULL if none is found.

struct json_object *csonpath_find_all(struct csonpath *p, struct json_object *json);

Return a new json_object array containing all matches. Must be freed with json_object_put().

int csonpath_remove(struct csonpath *p, struct json_object *json);

Remove all matching elements. Returns the number of elements removed.

int csonpath_update_or_create(struct csonpath *p, struct json_object *json, struct json_object *new_val);

Replace matching values with new_val, or create the path if it does not exist.

int csonpath_callback(struct csonpath *p, struct json_object *json,
                      json_c_callback callback, void *userdata);

Invoke callback for every match.

int csonpath_update_or_create_callback(struct csonpath *p, struct json_object *json,
                                       json_c_callback callback, void *userdata);

Like callback, but traverses the path while updating/creating missing intermediate objects.

void csonpath_destroy(struct csonpath *p);

Free the csonpath object.


📗 Python API Reference

  • CsonPath(path, return_empty_array=False, jq_like=False) — Create a new csonpath object. Optional flags: return_empty_array returns [] instead of None when find_all() finds nothing; jq_like allows jq-style paths without a leading $.
  • set_path(path) — Change the path expression.
  • find_first(json) — Return the first match, or None.
  • find_all(json) — Return a list of all matches, or None (or [] if configured).
  • remove(json) — Remove all matches. Returns the number of removed items.
  • update_or_create(json, value) — Replace matches with value, or create the path.
  • callback(json, callback, callback_data=None) — Call callback(parent, idx, current, callback_data) for every match.
  • update_or_create_callback(json, callback, callback_data=None) — Same as callback, but creates missing parent objects along the path.

🖥️ CLI

A standalone C CLI is available in cli/csonpath_cli.c. It links directly against json-c and the csonpath C core, so it works without Python.

make csonpath          # build ./csonpath
make tests-cli         # run shell tests

It reads JSON from stdin, a file (-f), or a string (-s) and exposes the library operations through action flags.

# Get the first match (default)
echo '{"a": "value", "array": [1, 2, 3]}' | ./csonpath '$.a'
# => "value"

# Find all matches
echo '{"items": [{"price": 5}, {"price": 15}]}' | ./csonpath -a '$.items[?price > 10]'
# => [{"price":15}]

# One match per line
echo '{"array": [1, 2, 3]}' | ./csonpath -a -o lines '$.array[*]'
# => 1
# => 2
# => 3

# Set or create a value
echo '{"a": 1}' | ./csonpath --set '42' '$.x.y.z'
# => {"a":1,"x":{"y":{"z":42}}}

# Or with a positional value
echo '{"a": 1}' | ./csonpath '$.x.y.z' '42'

# Remove matches
echo '{"a": 1, "b": 2}' | ./csonpath -d '$.b'
# => {"a":1}

# Edit a file in place
./csonpath -f data.json -p -i --set '"2.0"' '$.version'

Options

Option Description
-a, --all Return all matches instead of the first one.
-d, --delete Remove matches and print the modified JSON.
--set VALUE Set PATH to VALUE (JSON).
VALUE (positional) Alternative to --set.
-r, --raw Treat the value as a raw string.
-i, --in-place Edit FILE in place (requires --file).
--strict Exit with an error if --delete removes nothing.
-f FILE, --file FILE Read JSON from FILE instead of stdin.
-s JSON, --string JSON Read JSON from a string.
-j, --jq-like Allow jq-style paths without a leading $.
-p, --pretty Pretty-print JSON output.
-o {json,pretty,raw,lines} Output format (default: json).
-e, --empty-array Return [] instead of nothing when -a matches nothing.

Exit codes

Code Meaning
0 Success.
1 No match found, or --strict delete found nothing.
EINVAL Usage error, JSON parse error, JSONPath compilation error, or invalid JSON value.
errno I/O error (e.g. ENOENT, EACCES).

🔌 Custom Backends

csonpath is designed to be backend-agnostic. It can work with any data structure that supports array, object, and scalar semantics.

To create a custom backend, define the required macros and types in a header file (similar to csonpath_json-c.h or csonpath_python.c), then include your backend header before csonpath.h. This allows you to adapt csonpath for manipulating data in any format that supports array/object semantics, giving you full flexibility beyond just JSON.

For more details, see the existing backend implementations:

  • csonpath_json-c.h — json-c backend
  • csonpath_yyjson.h — yyjson backend
  • csonpath_python.c — Python backend
  • rust/csonpath_rust_backend.h — Rust / serde_json backend

🧪 Running Tests

C Tests

make tests-c

Rust Tests

cd rust
cargo test

Python Tests

make tests-py

All Tests

make tests

📁 Directory Structure

File / Directory Description
csonpath.h, csonpath_do.h Core implementation (header-only style)
csonpath_json-c.h json-c backend
csonpath_yyjson.h yyjson backend
csonpath_python.c Python C extension backend
rust/ Rust backend (safe API, FFI, C glue)
csonpath_my_fuzzing.h Fuzzer helpers (C)
tests/ C and Python test suites
bench/ Performance benchmarks

🤝 Contributing

We welcome contributions!

Please read our Contributing Guidelines and Code of Conduct before submitting a pull request.

Feel free to open issues or pull requests!


📜 License

BSD 3-Clause. See LICENSE.