jsonx 0.1.0

A serde-enabled implementation of the JSONX extended-JSON format: typed value constructors, unquoted keys, and trailing commas.
Documentation

jsonx

A serde-enabled Rust implementation of the 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

{
  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:

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).

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 = "...")].

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.

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:

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.