Skip to main content

tokenfold_core/transforms/
json.rs

1//! `json_minify` transform (canonical id `"json_minify"`, v1.0.0).
2//!
3//! Strips insignificant JSON whitespace (spaces, tabs, `\n`, `\r` that fall *outside*
4//! string literals) without ever:
5//! - reordering object keys,
6//! - changing string content or escape sequences, or
7//! - renormalizing number spelling (`1.0` must stay `1.0`, `1e10` must stay `1e10`).
8//!
9//! This is deliberately **not** implemented as parse-then-reserialize through
10//! `serde_json::Value`. Re-serializing a `Value` would risk exactly the failure modes
11//! above: `serde_json`'s number type does not remember whether `1.0` was spelled with a
12//! trailing `.0` or an exponent, and losing that spelling would change the byte identity
13//! of prompts that providers may cache on. Key order is only preserved through
14//! `Value` if the `preserve_order` feature is enabled crate-wide, which is a global,
15//! easy-to-silently-break invariant; recovering that in a `Value` layer would be nice, but the
16//! two problems above (numbers, and depending on a global feature flag for correctness)
17//! are still true.
18//!
19//! Instead, this transform:
20//! 1. Validates the input by parsing it into a `serde_json::Value` and immediately
21//!    discarding it. This enforces valid UTF-8 and valid JSON grammar (rejecting things
22//!    like unterminated strings) without ever using the parsed value for output.
23//! 2. Performs a single lexical pass over the original bytes, tracking whether the
24//!    cursor is inside a string literal (so whitespace inside strings is never touched)
25//!    and whether the current byte is escaped (so an escaped quote `\"` does not
26//!    prematurely end the string).
27
28/// Canonical transform id, as registered with the pipeline.
29pub const TRANSFORM_ID: &str = "json_minify";
30
31/// Semantic version of this transform's output behavior.
32pub const TRANSFORM_VERSION: &str = "1.0.0";
33
34/// Strips insignificant JSON whitespace from `input`, preserving key order, string
35/// content/escapes, and number spelling exactly.
36///
37/// Empty input is a special case: `serde_json` treats `b""` as invalid JSON, but this
38/// transform must be a no-op (not an error) on empty input, so it short-circuits before
39/// validation.
40pub fn minify_json(input: &[u8]) -> Result<Vec<u8>, JsonMinifyError> {
41    if input.is_empty() {
42        return Ok(Vec::new());
43    }
44
45    // Validate only; the parsed `Value` is discarded. This is what rejects invalid
46    // UTF-8, malformed JSON, and unterminated strings before the lexical pass below
47    // ever runs.
48    serde_json::from_slice::<serde_json::Value>(input)?;
49
50    let mut out = Vec::with_capacity(input.len());
51    let mut in_string = false;
52    let mut escaped = false;
53
54    for &byte in input {
55        if in_string {
56            out.push(byte);
57            if escaped {
58                escaped = false;
59            } else if byte == b'\\' {
60                escaped = true;
61            } else if byte == b'"' {
62                in_string = false;
63            }
64        } else {
65            match byte {
66                b'"' => {
67                    in_string = true;
68                    out.push(byte);
69                }
70                b' ' | b'\n' | b'\r' | b'\t' => {}
71                _ => out.push(byte),
72            }
73        }
74    }
75
76    Ok(out)
77}
78
79#[derive(Debug, thiserror::Error)]
80pub enum JsonMinifyError {
81    #[error("invalid json: {0}")]
82    Invalid(#[from] serde_json::Error),
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn minified_output_is_still_valid_json() {
91        let input = br#"{ "a" : [1, 2, 3], "b" : { "c" : null } }"#;
92        let out = minify_json(input).unwrap();
93
94        let original: serde_json::Value = serde_json::from_slice(input).unwrap();
95        let minified: serde_json::Value = serde_json::from_slice(&out).unwrap();
96        assert_eq!(original, minified);
97    }
98
99    #[test]
100    fn key_order_is_preserved_byte_for_byte() {
101        let input = b"{\"b\": 1, \"a\": 2}";
102        let out = minify_json(input).unwrap();
103        // Must be exactly this - NOT reordered to {"a":2,"b":1}.
104        assert_eq!(out, b"{\"b\":1,\"a\":2}");
105    }
106
107    #[test]
108    fn duplicate_keys_are_not_deduplicated() {
109        // `minify_json` is a lexical whitespace strip, not a parser: it does not
110        // deduplicate repeated keys. Collapsing duplicate keys into a single entry is
111        // the responsibility of whoever later parses these bytes into a map.
112        let input = b"{\"a\": 1, \"a\": 2}";
113        let out = minify_json(input).unwrap();
114        assert_eq!(out, b"{\"a\":1,\"a\":2}");
115    }
116
117    #[test]
118    fn string_content_and_escapes_survive_byte_for_byte() {
119        // `br#"..."#` is a raw byte string literal: every backslash below is a literal
120        // source byte (a JSON newline escape, an escaped quote, an escaped backslash,
121        // and a six-character JSON unicode escape sequence), not a Rust string escape.
122        // They must reach the output completely unchanged - the transform never
123        // decodes or re-encodes them.
124        let input: &[u8] = br#"{"s": "line1\nline2 \"quoted\" back\\slash unicode\u00e9"}"#;
125        let out = minify_json(input).unwrap();
126        assert_eq!(
127            out,
128            br#"{"s":"line1\nline2 \"quoted\" back\\slash unicode\u00e9"}"#.to_vec()
129        );
130    }
131
132    #[test]
133    fn number_spelling_is_never_renormalized() {
134        let input = b"{ \"x\" : 1.0 , \"y\" : 1e10 }";
135        let out = minify_json(input).unwrap();
136        let out_str = std::str::from_utf8(&out).unwrap();
137        assert_eq!(out_str, "{\"x\":1.0,\"y\":1e10}");
138        assert!(out_str.contains("1.0"));
139        assert!(out_str.contains("1e10"));
140        assert!(!out_str.contains("10000000000"));
141    }
142
143    #[test]
144    fn non_utf8_input_is_rejected() {
145        let input: &[u8] = &[0xFF, 0xFE, 0x00];
146        let err = minify_json(input).unwrap_err();
147        assert!(matches!(err, JsonMinifyError::Invalid(_)));
148    }
149
150    #[test]
151    fn empty_input_returns_empty_output_not_an_error() {
152        let out = minify_json(b"").unwrap();
153        assert!(out.is_empty());
154    }
155
156    #[test]
157    fn unterminated_string_is_rejected() {
158        let input = b"{\"a\": \"b";
159        let err = minify_json(input).unwrap_err();
160        assert!(matches!(err, JsonMinifyError::Invalid(_)));
161    }
162
163    #[test]
164    fn minify_is_idempotent_on_whitespace_heavy_nested_input() {
165        let input =
166            b"{\n  \"outer\" : {\n    \"list\" : [ 1,  2,\t3 ],\n    \"s\" : \"a\\tb\"\n  }\n}\n";
167        let once = minify_json(input).unwrap();
168        let twice = minify_json(&once).unwrap();
169        assert_eq!(once, twice);
170    }
171}