# jsonx
A [serde](https://serde.rs)-enabled Rust implementation of the
[`dop251/jsonx`](https://github.com/dop251/jsonx) format --
a superset of JSON that relaxes the syntax and adds typed value constructors.
Ported in spirit (not line-for-line) from the Go package.
JSONX is (mostly) used as a configuration format, so this implementation
does not pursue the maximum performance. It is, however, on par with serde_json.
The library is properly tested and fuzzed and overall is safe to use.
## What JSONX adds on top of JSON
- Unquoted object keys matching `^[A-Za-z_][0-9A-Za-z_]*$`.
- Trailing commas after the last array/object element.
- Typed value constructors written as `type(value)`:
- sized integers -- `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`,
`uint32`, `uint64`, plus machine-width `int` / `uint`;
- `datetime("2017-12-25T15:00:00Z")` (RFC 3339);
- `ip("192.168.1.2")` / `ip("::1")`;
- `ipport("192.168.1.2:65000")` / `ipport("[::1]:65000")`;
- `bytes("YWJjZA==")` (standard Base64).
Plain JSON numbers always decode to an `f64`, exactly as in the reference
implementation. JSONX is itself a valid ES5 expression, so a document can be
`eval()`-ed in JavaScript given definitions of the `type()` functions.
## Example
```js
{
k01: null,
k04: "test",
k05: 1.45678e-98,
k06: int(-454365464),
k14: int64("9223372036854775807"),
k16: datetime("2017-12-25T15:00:00Z"),
k17: ip("192.168.1.2"),
k20: ipport("[::1]:65000"),
k21: bytes("YWJjZA=="),
nested: { test: true },
}
```
## Usage
Work with arbitrary documents through `Value`:
```rust
let value: jsonx::Value =
jsonx::from_str(r#"{ id: int(7), tags: ["a", "b",] }"#).unwrap();
assert_eq!(value.get("id"), Some(&jsonx::Value::Int(7)));
let text = jsonx::to_string(&value).unwrap();
assert_eq!(text, r#"{id:int(7),tags:["a","b"]}"#);
let pretty = jsonx::to_string_pretty(&value).unwrap();
```
Or derive `Serialize` / `Deserialize` on your own types. Rust's integer types
map to the matching JSONX sized integers. For the other extended types, use the
wrapper types `Bytes`, `Int`, `Uint`, `Ip`, `IpPort`, and `Datetime`: they need
no attribute and -- because they stay transparent in other serde formats -- let
the same struct serialize cleanly to JSONX and to, say, TOML or JSON
(where they degrade to plain strings/integers).
```rust
use jsonx::{Bytes, Ip};
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct Host {
name: String,
port: u16,
addr: Ip,
blob: Bytes,
}
let host = Host {
name: "db".into(),
port: 5432,
addr: Ip("10.0.0.1".parse().unwrap()),
blob: Bytes(b"hi".to_vec()),
};
let text = jsonx::to_string(&host).unwrap();
// {name:"db",port:uint16(5432),addr:ip("10.0.0.1"),blob:bytes("aGk=")}
assert_eq!(jsonx::from_str::<Host>(&text).unwrap(), host);
```
If you must keep a bare `chrono::DateTime` or `std::net` address field, the
`#[serde(with = "jsonx::datetime")]`, `#[serde(with = "jsonx::ip")]`, and
`#[serde(with = "jsonx::ipport")]` modules provide the same forms via an
attribute.
### Extending JSONX with your own types
JSONX is open: a document is a valid ES5 expression, so `myType(value)` is just
a function call. Give one of your own types a `type(value)` form and it renders
as a constructor in JSONX while staying a plain string everywhere else.
If the type implements `Display` + `FromStr`, `#[derive(JsonxConstructor)]`
generates the trait and the serde impls for you (default `derive` feature). The
constructor name defaults to the type name lowercased; override with
`#[jsonx(name = "...")]`.
```rust
use std::{fmt, str::FromStr};
#[derive(jsonx::JsonxConstructor, Debug, PartialEq)]
#[jsonx(name = "semver")]
struct Version(u16, u16);
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.0, self.1)
}
}
impl FromStr for Version {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
let (a, b) = s.split_once('.').ok_or("expected MAJOR.MINOR")?;
Ok(Version(a.parse().map_err(|_| "bad major")?, b.parse().map_err(|_| "bad minor")?))
}
}
assert_eq!(jsonx::to_string(&Version(1, 4)).unwrap(), r#"semver("1.4")"#);
```
When a string representation doesn't fit, implement `JsonxConstructor` by hand
instead (`TOKEN` via the `ctor!` macro, plus `to_jsonx_arg`/`from_jsonx_arg`)
and delegate serde to `jsonx::constructor::{serialize, deserialize}` -- see the
`constructor` module docs.
For a foreign type you don't own (e.g. `uuid::Uuid`), wrap it in a newtype and
derive/implement on the wrapper -- just as `Ip` wraps `IpAddr`.
Openness extends to the dynamic path too: decoding into `Value` captures *any*
unrecognized `name(value)` as `Value::Constructor { name, arg }` (rather than
failing), so generic tooling can pass through, inspect, and re-emit custom
constructors losslessly without knowing them ahead of time. A constructor `name`
must be a JSONX identifier (a leading letter or `_`, then letters, digits, or
`_`); a name that isn't has no round-trippable form, so serializing it errors
rather than emitting unparseable output.
```rust
let v: jsonx::Value = jsonx::from_str(r#"ipnet("10.0.0.0/8")"#).unwrap();
assert_eq!(jsonx::to_string(&v).unwrap(), r#"ipnet("10.0.0.0/8")"#);
```
### Non-greedy decoding
`from_str_partial` decodes a single value and reports where it stopped:
```rust
let (value, offset): (jsonx::Value, usize) =
jsonx::from_str_partial("{test: 1} blah").unwrap();
assert_eq!(&"{test: 1} blah"[offset..], "blah");
```
## API surface
- `from_str`, `from_slice`, `from_str_partial`
- `to_string`, `to_string_pretty`, `to_string_indent`, `to_vec`,
`to_writer`, `to_writer_pretty`
- `Value` (including `Value::Constructor` and the `Value::{int, uint, bytes,
string, constructor}` builders and `Value::to_jsonx_arg`), `Map`, `DateTime`
- wrappers: `Bytes`, `Int`, `Uint`, `Ip`, `IpPort`, `Datetime` (each a public
`JsonxConstructor`, so `Datetime(dt).to_jsonx_arg()` etc. yields the canonical
argument text)
- `#[serde(with)]` modules: `ip`, `ipport`, `datetime` (plus the public
`datetime::to_jsonx_string` / `datetime::parse` helpers)
- open extension: `#[derive(JsonxConstructor)]` (default `derive` feature), the
`JsonxConstructor` trait, the `ctor!` macro, and the `constructor` module
(`serialize` / `deserialize` for `#[serde(with)]`, plus the low-level
`serialize_constructor` / `deserialize_constructor`)
- `Serializer`, `Deserializer` for advanced/streaming use
- `Error`, `Result`
## Design notes
- The deserializer is streaming and borrows string data straight from the input
when a string contains no escapes.
- Object output is key-sorted by default (a `BTreeMap` backs `Value`), giving
deterministic, input-order-independent encodings. Enable the `preserve_order`
feature to back `Value` with `indexmap::IndexMap` and keep author key order
instead (matching the other encoders most projects pair this with).
- `int64`/`uint64` values outside the `±(2^53 − 1)` safe-integer range are
quoted (e.g. `int64("9223372036854775807")`) so they survive a round trip
through a JavaScript `Number`.
- Invalid UTF-8 and lone surrogates are rejected (Rust strings are UTF-8), and
parsing is bounded against deeply-nested adversarial input.
## License
Licensed under either of MIT or Apache-2.0 at your option.