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}