1use crate::backend;
15use crate::config::{JsonFraming, OnError};
16use crate::metrics::JsonDeserMetrics;
17use serde::de::DeserializeOwned;
18use serde_json::Value;
19use spate_core::checkpoint::AckRef;
20use spate_core::deser::{Deserializer, EmitRecord, Owned};
21use spate_core::error::DeserError;
22use spate_core::record::{RawPayload, Record};
23use spate_core::telemetry::RateLimit;
24use std::marker::PhantomData;
25use std::time::Duration;
26
27static SKIP_WARN: RateLimit = RateLimit::new(5, Duration::from_secs(10));
30
31#[derive(Clone, Copy)]
33enum Reason {
34 Malformed,
35 DuplicateKey,
36}
37
38impl Reason {
39 fn label(self) -> &'static str {
40 match self {
41 Reason::Malformed => "malformed",
42 Reason::DuplicateKey => "duplicate_key",
43 }
44 }
45}
46
47fn dup_check_reason(e: &backend::DecodeError) -> Reason {
53 if e.is_data {
54 Reason::DuplicateKey
55 } else {
56 Reason::Malformed
57 }
58}
59
60#[derive(Clone, Debug)]
62pub(crate) struct DecoderCore {
63 pub(crate) framing: JsonFraming,
64 pub(crate) on_error: OnError,
65 pub(crate) reject_duplicate_keys: bool,
66 pub(crate) metrics: Option<JsonDeserMetrics>,
67}
68
69impl DecoderCore {
70 fn run<'buf, P>(
72 &self,
73 raw: &RawPayload<'buf>,
74 ack: &AckRef,
75 out: &mut dyn EmitRecord<'buf, P>,
76 ) -> Result<(), DeserError>
77 where
78 P: DeserializeOwned,
79 {
80 match self.framing {
81 JsonFraming::Single => self.run_single(raw, ack, out),
82 JsonFraming::Ndjson => self.run_ndjson(raw, ack, out),
83 JsonFraming::Array => self.run_array(raw, ack, out),
84 }
85 }
86
87 fn run_single<'buf, P: DeserializeOwned>(
88 &self,
89 raw: &RawPayload<'buf>,
90 ack: &AckRef,
91 out: &mut dyn EmitRecord<'buf, P>,
92 ) -> Result<(), DeserError> {
93 if is_blank(raw.bytes) {
94 return Ok(()); }
96 if let Some(payload) = self.decode_item::<P>(raw.bytes)? {
97 let _ = out.emit(Record {
98 payload,
99 meta: raw.meta(),
100 ack: ack.clone(),
101 });
102 }
103 Ok(())
104 }
105
106 fn run_ndjson<'buf, P: DeserializeOwned>(
107 &self,
108 raw: &RawPayload<'buf>,
109 ack: &AckRef,
110 out: &mut dyn EmitRecord<'buf, P>,
111 ) -> Result<(), DeserError> {
112 match self.on_error {
115 OnError::Skip => {
118 for line in raw.bytes.split(|&b| b == b'\n') {
119 if is_blank(line) {
120 continue;
121 }
122 if let Some(payload) = self.decode_item::<P>(line)? {
123 let _ = out.emit(Record {
124 payload,
125 meta: raw.meta(),
126 ack: ack.clone(),
127 });
128 }
129 }
130 Ok(())
131 }
132 OnError::Fail => {
138 let mut decoded = Vec::new();
139 for line in raw.bytes.split(|&b| b == b'\n') {
140 if is_blank(line) {
141 continue;
142 }
143 if let Some(payload) = self.decode_item::<P>(line)? {
144 decoded.push(payload);
145 }
146 }
147 for payload in decoded {
148 let _ = out.emit(Record {
149 payload,
150 meta: raw.meta(),
151 ack: ack.clone(),
152 });
153 }
154 Ok(())
155 }
156 }
157 }
158
159 fn run_array<'buf, P: DeserializeOwned>(
160 &self,
161 raw: &RawPayload<'buf>,
162 ack: &AckRef,
163 out: &mut dyn EmitRecord<'buf, P>,
164 ) -> Result<(), DeserError> {
165 if is_blank(raw.bytes) {
166 return Ok(());
167 }
168 if self.reject_duplicate_keys
172 && let Err(e) = backend::check_no_duplicate_keys(raw.bytes)
173 {
174 return self.on_item_error(&e, dup_check_reason(&e));
175 }
176 match backend::decode_one::<Vec<P>>(raw.bytes) {
177 Ok(items) => {
178 for payload in items {
179 let _ = out.emit(Record {
180 payload,
181 meta: raw.meta(),
182 ack: ack.clone(),
183 });
184 }
185 Ok(())
186 }
187 Err(e) => self.on_item_error(&e, Reason::Malformed),
188 }
189 }
190
191 fn decode_item<P: DeserializeOwned>(&self, bytes: &[u8]) -> Result<Option<P>, DeserError> {
194 if self.reject_duplicate_keys
195 && let Err(e) = backend::check_no_duplicate_keys(bytes)
196 {
197 self.on_item_error(&e, dup_check_reason(&e))?;
198 return Ok(None);
199 }
200 match backend::decode_one::<P>(bytes) {
201 Ok(payload) => Ok(Some(payload)),
202 Err(e) => {
203 self.on_item_error(&e, Reason::Malformed)?;
204 Ok(None)
205 }
206 }
207 }
208
209 fn on_item_error(&self, e: &backend::DecodeError, reason: Reason) -> Result<(), DeserError> {
212 match self.on_error {
213 OnError::Skip => {
214 if let Some(m) = &self.metrics {
215 match reason {
216 Reason::Malformed => m.dropped_malformed(),
217 Reason::DuplicateKey => m.dropped_duplicate_key(),
218 }
219 }
220 spate_core::rate_limited_warn!(
221 SKIP_WARN,
222 reason = reason.label(),
223 error = %e,
224 "json record skipped by on_error policy"
225 );
226 Ok(())
227 }
228 OnError::Fail => Err(DeserError::Malformed {
229 reason: format!("{}: {e}", reason.label()),
230 }),
231 }
232 }
233}
234
235fn is_blank(bytes: &[u8]) -> bool {
238 bytes.iter().all(u8::is_ascii_whitespace)
239}
240
241pub struct JsonSerdeDeserializer<T> {
245 core: DecoderCore,
246 _t: PhantomData<fn() -> T>,
247}
248
249impl<T> Clone for JsonSerdeDeserializer<T> {
252 fn clone(&self) -> Self {
253 JsonSerdeDeserializer {
254 core: self.core.clone(),
255 _t: PhantomData,
256 }
257 }
258}
259
260impl<T> std::fmt::Debug for JsonSerdeDeserializer<T> {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 f.debug_struct("JsonSerdeDeserializer")
263 .field("core", &self.core)
264 .finish_non_exhaustive()
265 }
266}
267
268impl<T> JsonSerdeDeserializer<T> {
269 pub(crate) fn new(core: DecoderCore) -> Self {
270 JsonSerdeDeserializer {
271 core,
272 _t: PhantomData,
273 }
274 }
275}
276
277impl<T> Deserializer<Owned<T>> for JsonSerdeDeserializer<T>
278where
279 T: DeserializeOwned + Send + 'static,
280{
281 fn deserialize<'buf>(
282 &mut self,
283 raw: &RawPayload<'buf>,
284 ack: &AckRef,
285 out: &mut dyn EmitRecord<'buf, T>,
286 ) -> Result<(), DeserError> {
287 self.core.run::<T>(raw, ack, out)
288 }
289}
290
291#[derive(Clone, Debug)]
294pub struct JsonValueDeserializer {
295 core: DecoderCore,
296}
297
298impl JsonValueDeserializer {
299 pub(crate) fn new(core: DecoderCore) -> Self {
300 JsonValueDeserializer { core }
301 }
302}
303
304impl Deserializer<Owned<Value>> for JsonValueDeserializer {
305 fn deserialize<'buf>(
306 &mut self,
307 raw: &RawPayload<'buf>,
308 ack: &AckRef,
309 out: &mut dyn EmitRecord<'buf, Value>,
310 ) -> Result<(), DeserError> {
311 self.core.run::<Value>(raw, ack, out)
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::config::{JsonDeserializerBuilder, JsonSettings};
319 use serde::Deserialize;
320 use spate_core::record::Flow;
321 use spate_test::render_metrics;
322
323 #[derive(Debug, Deserialize, PartialEq)]
324 struct Ev {
325 id: i64,
326 name: String,
327 }
328
329 struct Collected<T>(Vec<Record<T>>);
330 impl<'buf, T> EmitRecord<'buf, T> for Collected<T> {
331 fn emit(&mut self, rec: Record<T>) -> Flow {
332 self.0.push(rec);
333 Flow::Continue
334 }
335 }
336
337 fn raw(bytes: &[u8]) -> RawPayload<'_> {
338 RawPayload {
339 offset: 42,
340 ..spate_test::raw_payload(bytes)
341 }
342 }
343
344 use spate_test::test_ack;
345
346 fn builder(framing: JsonFraming, on_error: OnError, dup: bool) -> JsonDeserializerBuilder {
347 JsonDeserializerBuilder::from_settings(JsonSettings {
348 framing,
349 on_error,
350 reject_duplicate_keys: dup,
351 })
352 }
353
354 #[test]
355 fn single_round_trip_and_meta() {
356 let mut d = builder(JsonFraming::Single, OnError::Skip, false).build_serde::<Ev>();
357 let mut out = Collected(Vec::new());
358 d.deserialize(&raw(br#"{"id":7,"name":"orders"}"#), &test_ack(), &mut out)
359 .unwrap();
360 assert_eq!(out.0.len(), 1);
361 assert_eq!(out.0[0].meta.offset, 42);
362 assert_eq!(
363 out.0[0].payload,
364 Ev {
365 id: 7,
366 name: "orders".into()
367 }
368 );
369 }
370
371 #[test]
372 fn ndjson_emits_a_record_per_line() {
373 let mut d = builder(JsonFraming::Ndjson, OnError::Skip, false).build_serde::<Ev>();
374 let mut out = Collected(Vec::new());
375 let payload =
376 b"{\"id\":1,\"name\":\"a\"}\n{\"id\":2,\"name\":\"b\"}\n{\"id\":3,\"name\":\"c\"}";
377 d.deserialize(&raw(payload), &test_ack(), &mut out).unwrap();
378 assert_eq!(out.0.len(), 3);
379 assert_eq!(out.0[2].payload.id, 3);
380 assert!(out.0.iter().all(|r| r.meta.offset == 42));
382 }
383
384 #[test]
385 fn array_explodes_elements() {
386 let mut d = builder(JsonFraming::Array, OnError::Skip, false).build_serde::<Ev>();
387 let mut out = Collected(Vec::new());
388 d.deserialize(
389 &raw(br#"[{"id":1,"name":"a"},{"id":2,"name":"b"}]"#),
390 &test_ack(),
391 &mut out,
392 )
393 .unwrap();
394 assert_eq!(out.0.len(), 2);
395 }
396
397 #[test]
398 fn empty_and_blank_payloads_are_tombstones() {
399 let mut d = builder(JsonFraming::Single, OnError::Skip, false).build_value();
400 let mut out = Collected(Vec::new());
401 d.deserialize(&raw(b""), &test_ack(), &mut out).unwrap();
402 d.deserialize(&raw(b" \n "), &test_ack(), &mut out)
403 .unwrap();
404 assert!(out.0.is_empty());
405 }
406
407 #[test]
408 fn malformed_single_skips_or_fails() {
409 let mut skip = builder(JsonFraming::Single, OnError::Skip, false).build_serde::<Ev>();
410 let mut out = Collected(Vec::new());
411 skip.deserialize(&raw(b"{not json"), &test_ack(), &mut out)
412 .unwrap();
413 assert!(out.0.is_empty(), "skip drops the bad payload");
414
415 let mut fail = builder(JsonFraming::Single, OnError::Fail, false).build_serde::<Ev>();
416 let err = fail
417 .deserialize(&raw(b"{not json"), &test_ack(), &mut out)
418 .unwrap_err();
419 assert!(matches!(err, DeserError::Malformed { .. }), "{err}");
420 }
421
422 #[test]
423 fn ndjson_skip_isolates_the_bad_line() {
424 let mut d = builder(JsonFraming::Ndjson, OnError::Skip, false).build_serde::<Ev>();
425 let mut out = Collected(Vec::new());
426 let payload = b"{\"id\":1,\"name\":\"a\"}\nGARBAGE\n{\"id\":3,\"name\":\"c\"}";
427 d.deserialize(&raw(payload), &test_ack(), &mut out).unwrap();
428 assert_eq!(out.0.len(), 2, "good lines flow, bad line dropped");
429 assert_eq!(out.0[0].payload.id, 1);
430 assert_eq!(out.0[1].payload.id, 3);
431 }
432
433 #[test]
434 fn ndjson_fail_stops_on_the_bad_line() {
435 let mut d = builder(JsonFraming::Ndjson, OnError::Fail, false).build_serde::<Ev>();
436 let mut out = Collected(Vec::new());
437 let payload = b"{\"id\":1,\"name\":\"a\"}\nGARBAGE\n{\"id\":3,\"name\":\"c\"}";
438 let err = d
439 .deserialize(&raw(payload), &test_ack(), &mut out)
440 .unwrap_err();
441 assert!(matches!(err, DeserError::Malformed { .. }), "{err}");
442 assert!(
445 out.0.is_empty(),
446 "no record may be emitted when the payload fails"
447 );
448 }
449
450 #[test]
451 fn array_error_is_atomic() {
452 let mut d = builder(JsonFraming::Array, OnError::Skip, false).build_serde::<Ev>();
453 let mut out = Collected(Vec::new());
454 d.deserialize(
456 &raw(br#"[{"id":1,"name":"a"},{"oops":true}]"#),
457 &test_ack(),
458 &mut out,
459 )
460 .unwrap();
461 assert!(out.0.is_empty(), "array error handling is atomic");
462 }
463
464 #[test]
465 fn duplicate_keys_last_wins_by_default_reject_when_configured() {
466 let mut allow = builder(JsonFraming::Single, OnError::Skip, false).build_value();
468 let mut out = Collected(Vec::new());
469 allow
470 .deserialize(&raw(br#"{"id":1,"id":2}"#), &test_ack(), &mut out)
471 .unwrap();
472 assert_eq!(out.0.len(), 1);
473 assert_eq!(out.0[0].payload["id"], serde_json::json!(2));
474
475 let mut reject = builder(JsonFraming::Single, OnError::Fail, true).build_value();
477 let err = reject
478 .deserialize(&raw(br#"{"id":1,"id":2}"#), &test_ack(), &mut out)
479 .unwrap_err();
480 match err {
481 DeserError::Malformed { reason } => assert!(
482 reason.starts_with("duplicate_key:"),
483 "a real duplicate key must be labeled duplicate_key, got `{reason}`"
484 ),
485 other => panic!("expected Malformed, got {other}"),
486 }
487 }
488
489 #[test]
490 fn nested_duplicate_keys_are_rejected() {
491 let mut d = builder(JsonFraming::Single, OnError::Fail, true).build_value();
492 let mut out = Collected(Vec::new());
493 let err = d
494 .deserialize(&raw(br#"{"outer":{"a":1,"a":2}}"#), &test_ack(), &mut out)
495 .unwrap_err();
496 assert!(matches!(err, DeserError::Malformed { .. }), "{err}");
497 }
498
499 #[test]
500 fn malformed_with_dedup_is_labeled_malformed_not_duplicate_key() {
501 let mut d = builder(JsonFraming::Single, OnError::Fail, true).build_serde::<Ev>();
504 let mut out = Collected(Vec::new());
505 let err = d
506 .deserialize(&raw(b"{not valid json"), &test_ack(), &mut out)
507 .unwrap_err();
508 match err {
509 DeserError::Malformed { reason } => assert!(
510 reason.starts_with("malformed:"),
511 "a syntax error must be labeled malformed, got `{reason}`"
512 ),
513 other => panic!("expected Malformed, got {other}"),
514 }
515 }
516
517 #[test]
518 fn skipped_records_are_counted_in_metrics() {
519 const STD: &str = r#"pipeline="orders",component="main",component_type="deserializer""#;
520 let rendered = render_metrics(|| {
521 let mut d = builder(JsonFraming::Ndjson, OnError::Skip, false)
522 .with_metrics("orders", "main")
523 .build_serde::<Ev>();
524 let mut out = Collected(Vec::new());
525 let payload =
526 b"{\"id\":1,\"name\":\"a\"}\nGARBAGE\nALSO BAD\n{\"id\":4,\"name\":\"d\"}";
527 d.deserialize(&raw(payload), &test_ack(), &mut out).unwrap();
528 assert_eq!(out.0.len(), 2);
529 });
530 let needle =
531 format!(r#"spate_json_deser_records_dropped_total{{{STD},reason="malformed"}} 2"#);
532 assert!(
533 rendered.contains(&needle),
534 "missing `{needle}`:\n{rendered}"
535 );
536 }
537}