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, PartitionId};
321
322 #[derive(Debug, Deserialize, PartialEq)]
323 struct Ev {
324 id: i64,
325 name: String,
326 }
327
328 struct Collected<T>(Vec<Record<T>>);
329 impl<'buf, T> EmitRecord<'buf, T> for Collected<T> {
330 fn emit(&mut self, rec: Record<T>) -> Flow {
331 self.0.push(rec);
332 Flow::Continue
333 }
334 }
335
336 fn raw(bytes: &[u8]) -> RawPayload<'_> {
337 RawPayload {
338 bytes,
339 key: Some(b"k"),
340 partition: PartitionId(3),
341 offset: 42,
342 timestamp_ms: 1_000,
343 }
344 }
345
346 fn test_ack() -> AckRef {
347 AckRef::test_pair().0
348 }
349
350 fn builder(framing: JsonFraming, on_error: OnError, dup: bool) -> JsonDeserializerBuilder {
351 JsonDeserializerBuilder::from_settings(JsonSettings {
352 framing,
353 on_error,
354 reject_duplicate_keys: dup,
355 })
356 }
357
358 #[test]
359 fn single_round_trip_and_meta() {
360 let mut d = builder(JsonFraming::Single, OnError::Skip, false).build_serde::<Ev>();
361 let mut out = Collected(Vec::new());
362 d.deserialize(&raw(br#"{"id":7,"name":"orders"}"#), &test_ack(), &mut out)
363 .unwrap();
364 assert_eq!(out.0.len(), 1);
365 assert_eq!(out.0[0].meta.offset, 42);
366 assert_eq!(
367 out.0[0].payload,
368 Ev {
369 id: 7,
370 name: "orders".into()
371 }
372 );
373 }
374
375 #[test]
376 fn ndjson_emits_a_record_per_line() {
377 let mut d = builder(JsonFraming::Ndjson, OnError::Skip, false).build_serde::<Ev>();
378 let mut out = Collected(Vec::new());
379 let payload =
380 b"{\"id\":1,\"name\":\"a\"}\n{\"id\":2,\"name\":\"b\"}\n{\"id\":3,\"name\":\"c\"}";
381 d.deserialize(&raw(payload), &test_ack(), &mut out).unwrap();
382 assert_eq!(out.0.len(), 3);
383 assert_eq!(out.0[2].payload.id, 3);
384 assert!(out.0.iter().all(|r| r.meta.offset == 42));
386 }
387
388 #[test]
389 fn array_explodes_elements() {
390 let mut d = builder(JsonFraming::Array, OnError::Skip, false).build_serde::<Ev>();
391 let mut out = Collected(Vec::new());
392 d.deserialize(
393 &raw(br#"[{"id":1,"name":"a"},{"id":2,"name":"b"}]"#),
394 &test_ack(),
395 &mut out,
396 )
397 .unwrap();
398 assert_eq!(out.0.len(), 2);
399 }
400
401 #[test]
402 fn empty_and_blank_payloads_are_tombstones() {
403 let mut d = builder(JsonFraming::Single, OnError::Skip, false).build_value();
404 let mut out = Collected(Vec::new());
405 d.deserialize(&raw(b""), &test_ack(), &mut out).unwrap();
406 d.deserialize(&raw(b" \n "), &test_ack(), &mut out)
407 .unwrap();
408 assert!(out.0.is_empty());
409 }
410
411 #[test]
412 fn malformed_single_skips_or_fails() {
413 let mut skip = builder(JsonFraming::Single, OnError::Skip, false).build_serde::<Ev>();
414 let mut out = Collected(Vec::new());
415 skip.deserialize(&raw(b"{not json"), &test_ack(), &mut out)
416 .unwrap();
417 assert!(out.0.is_empty(), "skip drops the bad payload");
418
419 let mut fail = builder(JsonFraming::Single, OnError::Fail, false).build_serde::<Ev>();
420 let err = fail
421 .deserialize(&raw(b"{not json"), &test_ack(), &mut out)
422 .unwrap_err();
423 assert!(matches!(err, DeserError::Malformed { .. }), "{err}");
424 }
425
426 #[test]
427 fn ndjson_skip_isolates_the_bad_line() {
428 let mut d = builder(JsonFraming::Ndjson, OnError::Skip, false).build_serde::<Ev>();
429 let mut out = Collected(Vec::new());
430 let payload = b"{\"id\":1,\"name\":\"a\"}\nGARBAGE\n{\"id\":3,\"name\":\"c\"}";
431 d.deserialize(&raw(payload), &test_ack(), &mut out).unwrap();
432 assert_eq!(out.0.len(), 2, "good lines flow, bad line dropped");
433 assert_eq!(out.0[0].payload.id, 1);
434 assert_eq!(out.0[1].payload.id, 3);
435 }
436
437 #[test]
438 fn ndjson_fail_stops_on_the_bad_line() {
439 let mut d = builder(JsonFraming::Ndjson, OnError::Fail, false).build_serde::<Ev>();
440 let mut out = Collected(Vec::new());
441 let payload = b"{\"id\":1,\"name\":\"a\"}\nGARBAGE\n{\"id\":3,\"name\":\"c\"}";
442 let err = d
443 .deserialize(&raw(payload), &test_ack(), &mut out)
444 .unwrap_err();
445 assert!(matches!(err, DeserError::Malformed { .. }), "{err}");
446 assert!(
449 out.0.is_empty(),
450 "no record may be emitted when the payload fails"
451 );
452 }
453
454 #[test]
455 fn array_error_is_atomic() {
456 let mut d = builder(JsonFraming::Array, OnError::Skip, false).build_serde::<Ev>();
457 let mut out = Collected(Vec::new());
458 d.deserialize(
460 &raw(br#"[{"id":1,"name":"a"},{"oops":true}]"#),
461 &test_ack(),
462 &mut out,
463 )
464 .unwrap();
465 assert!(out.0.is_empty(), "array error handling is atomic");
466 }
467
468 #[test]
469 fn duplicate_keys_last_wins_by_default_reject_when_configured() {
470 let mut allow = builder(JsonFraming::Single, OnError::Skip, false).build_value();
472 let mut out = Collected(Vec::new());
473 allow
474 .deserialize(&raw(br#"{"id":1,"id":2}"#), &test_ack(), &mut out)
475 .unwrap();
476 assert_eq!(out.0.len(), 1);
477 assert_eq!(out.0[0].payload["id"], serde_json::json!(2));
478
479 let mut reject = builder(JsonFraming::Single, OnError::Fail, true).build_value();
481 let err = reject
482 .deserialize(&raw(br#"{"id":1,"id":2}"#), &test_ack(), &mut out)
483 .unwrap_err();
484 match err {
485 DeserError::Malformed { reason } => assert!(
486 reason.starts_with("duplicate_key:"),
487 "a real duplicate key must be labeled duplicate_key, got `{reason}`"
488 ),
489 other => panic!("expected Malformed, got {other}"),
490 }
491 }
492
493 #[test]
494 fn nested_duplicate_keys_are_rejected() {
495 let mut d = builder(JsonFraming::Single, OnError::Fail, true).build_value();
496 let mut out = Collected(Vec::new());
497 let err = d
498 .deserialize(&raw(br#"{"outer":{"a":1,"a":2}}"#), &test_ack(), &mut out)
499 .unwrap_err();
500 assert!(matches!(err, DeserError::Malformed { .. }), "{err}");
501 }
502
503 #[test]
504 fn malformed_with_dedup_is_labeled_malformed_not_duplicate_key() {
505 let mut d = builder(JsonFraming::Single, OnError::Fail, true).build_serde::<Ev>();
508 let mut out = Collected(Vec::new());
509 let err = d
510 .deserialize(&raw(b"{not valid json"), &test_ack(), &mut out)
511 .unwrap_err();
512 match err {
513 DeserError::Malformed { reason } => assert!(
514 reason.starts_with("malformed:"),
515 "a syntax error must be labeled malformed, got `{reason}`"
516 ),
517 other => panic!("expected Malformed, got {other}"),
518 }
519 }
520
521 fn render(f: impl FnOnce()) -> String {
524 let recorder = metrics_exporter_prometheus::PrometheusBuilder::new().build_recorder();
525 let handle = recorder.handle();
526 metrics::with_local_recorder(&recorder, f);
527 handle.run_upkeep();
528 handle.render()
529 }
530
531 #[test]
532 fn skipped_records_are_counted_in_metrics() {
533 const STD: &str = r#"pipeline="orders",component="main",component_type="deserializer""#;
534 let rendered = render(|| {
535 let mut d = builder(JsonFraming::Ndjson, OnError::Skip, false)
536 .with_metrics("orders", "main")
537 .build_serde::<Ev>();
538 let mut out = Collected(Vec::new());
539 let payload =
540 b"{\"id\":1,\"name\":\"a\"}\nGARBAGE\nALSO BAD\n{\"id\":4,\"name\":\"d\"}";
541 d.deserialize(&raw(payload), &test_ack(), &mut out).unwrap();
542 assert_eq!(out.0.len(), 2);
543 });
544 let needle =
545 format!(r#"spate_json_deser_records_dropped_total{{{STD},reason="malformed"}} 2"#);
546 assert!(
547 rendered.contains(&needle),
548 "missing `{needle}`:\n{rendered}"
549 );
550 }
551}