moq_json/snapshot/encoder.rs
1//! The track-free half of snapshot publishing: values in, frame payloads out.
2
3use std::marker::PhantomData;
4
5use bytes::Bytes;
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::{Diff, Result, diff};
10
11/// Maximum frames (snapshot + deltas) in a single group before a new snapshot is forced.
12///
13/// Kept well below moq-net's per-group frame cap so a late joiner can always read the snapshot
14/// at frame 0 before the group is evicted.
15pub(super) const MAX_DELTA_FRAMES: usize = 256;
16
17/// Configuration for an [`Encoder`], and so for the [`Producer`](super::Producer) wrapping one.
18///
19/// Build from [`Default`] and override fields (the struct is `#[non_exhaustive]`, so new
20/// options stay additive), or chain the `with_*` setters.
21#[derive(Debug, Clone)]
22#[non_exhaustive]
23pub struct ProducerConfig {
24 /// Controls how aggressively the encoder emits deltas (merge patches) instead of full snapshots.
25 ///
26 /// A ratio of `0` disables deltas: every change is encoded as a new snapshot.
27 ///
28 /// A positive ratio enables deltas. A new snapshot is emitted once the deltas *already written*
29 /// to the current group (excluding the snapshot frame) exceed `ratio` times the snapshot size.
30 /// The pending delta is excluded from that check, so the one that first crosses the budget
31 /// still lands before the group rolls. So `1` allows roughly one snapshot's worth of deltas before
32 /// rolling, and a larger ratio tolerates more.
33 ///
34 /// When [`compression`](Self::compression) is on, both sides of the comparison are measured on
35 /// the *compressed* frame sizes (the real wire cost).
36 ///
37 /// Defaults to `8`.
38 pub delta_ratio: u32,
39
40 /// Compress each group as one sync-flushed DEFLATE stream, so deltas reuse the snapshot as
41 /// context and shrink sharply.
42 ///
43 /// `false` (the default) emits plaintext JSON frames, identical on the wire to an uncompressed
44 /// track. A [`Decoder`](super::Decoder) reading them must set
45 /// [`ConsumerConfig::compression`](super::ConsumerConfig::compression) to match.
46 pub compression: bool,
47}
48
49impl ProducerConfig {
50 /// Set [`delta_ratio`](Self::delta_ratio) (a builder, since the struct is `#[non_exhaustive]`).
51 pub fn with_delta_ratio(mut self, delta_ratio: u32) -> Self {
52 self.delta_ratio = delta_ratio;
53 self
54 }
55
56 /// Set [`compression`](Self::compression) (a builder, since the struct is `#[non_exhaustive]`).
57 pub fn with_compression(mut self, compression: bool) -> Self {
58 self.compression = compression;
59 self
60 }
61}
62
63impl Default for ProducerConfig {
64 fn default() -> Self {
65 Self {
66 delta_ratio: 8,
67 compression: false,
68 }
69 }
70}
71
72/// One encoded frame, and the group boundary it implies.
73#[derive(Clone, Debug)]
74pub struct Encoded {
75 /// The frame payload, DEFLATE-compressed when [`ProducerConfig::compression`] is set.
76 pub payload: Bytes,
77
78 /// Whether this frame is a full snapshot, which must open a new group.
79 ///
80 /// `true` means the caller writes it as the first frame of a fresh group; `false` means it is a
81 /// merge patch that must be appended to the group the last snapshot opened. Mapping straight onto
82 /// [`moq_mux::container::Frame::keyframe`] is the point of the name.
83 ///
84 /// The encoder decides this, never the caller: a value that sets a field to JSON null, or whose
85 /// root isn't an object, cannot be expressed as a merge patch at all, and the delta budget and
86 /// frame cap force a snapshot independently of what the caller wanted.
87 ///
88 /// [`moq_mux::container::Frame::keyframe`]: https://docs.rs/moq-mux/latest/moq_mux/container/struct.Frame.html
89 pub keyframe: bool,
90}
91
92/// An encoded frame the caller has not yet acknowledged writing.
93///
94/// Returned by [`Encoder::update`]. Read [`payload`](Encoded::payload) and
95/// [`keyframe`](Encoded::keyframe) through the [`Deref`](std::ops::Deref) to [`Encoded`], write the
96/// frame, then [`commit`](Self::commit).
97///
98/// Dropping it uncommitted [`Encoder::reset`]s, so a frame that never reached the wire leaves the
99/// encoder resynchronizing with a fresh snapshot rather than emitting deltas against a baseline no
100/// consumer received. Note that this is a recovery, not a rollback: producing a delta payload
101/// advances the group's DEFLATE window, and that can't be undone, so a snapshot is the only sound
102/// way back. Forgetting to commit a frame that *was* written is therefore merely wasteful (one
103/// redundant snapshot), never incorrect.
104#[must_use = "the frame must be written and committed, or dropped to resynchronize the encoder"]
105pub struct Pending<'a, T> {
106 encoder: &'a mut Encoder<T>,
107 encoded: Encoded,
108 committed: bool,
109}
110
111impl<T> Pending<'_, T> {
112 /// Acknowledge that the frame reached the wire, keeping the encoder's state.
113 ///
114 /// Only call this once the write has actually succeeded. Committing a frame that failed to write
115 /// is the one thing that corrupts the stream.
116 pub fn commit(mut self) {
117 self.committed = true;
118 }
119}
120
121impl<T> std::ops::Deref for Pending<'_, T> {
122 type Target = Encoded;
123
124 fn deref(&self) -> &Encoded {
125 &self.encoded
126 }
127}
128
129impl<T> Drop for Pending<'_, T> {
130 fn drop(&mut self) {
131 if !self.committed {
132 self.encoder.reset();
133 }
134 }
135}
136
137/// Encodes a JSON value into frame payloads, choosing snapshots and deltas automatically.
138///
139/// The track-free core of [`Producer`](super::Producer): it decides *what bytes go in a frame* and
140/// *where the group boundaries fall*, and leaves writing them to the caller. Reach for it when
141/// something else already owns the track, for example a
142/// [`moq_mux::container::Producer`](https://docs.rs/moq-mux/latest/moq_mux/container/struct.Producer.html)
143/// that is also managing a timeline and a catalog estimate:
144///
145/// ```ignore
146/// if let Some(frame) = encoder.update(&value)? {
147/// container.write(moq_mux::container::Frame {
148/// timestamp,
149/// duration: None,
150/// payload: frame.payload.clone(),
151/// keyframe: frame.keyframe,
152/// })?; // an early return here drops `frame`, resetting the encoder
153/// frame.commit();
154/// }
155/// ```
156///
157/// Frames must reach the wire in the order they were encoded, and a frame with
158/// [`keyframe`](Encoded::keyframe) set must open a new group: both the merge patches and the
159/// group-scoped DEFLATE window depend on it. [`update`](Self::update) hands back a [`Pending`]
160/// rather than a bare [`Encoded`] so a frame that never reaches the wire can't silently desync the
161/// encoder: dropping it uncommitted [`reset`](Self::reset)s, and the next value is encoded as a
162/// fresh snapshot. Committing a frame you failed to write is the one way to corrupt the stream.
163///
164/// If the caller cuts a group for its own reasons (a `cut`, `seek`, or discontinuity), call
165/// [`reset`](Self::reset) directly so the next value opens the new group with a snapshot.
166pub struct Encoder<T> {
167 config: ProducerConfig,
168
169 /// The last encoded value, the baseline every delta is diffed against. `None` until the first
170 /// snapshot, which is what makes that first [`update`](Self::update) a keyframe.
171 last: Option<Value>,
172
173 /// The current group's DEFLATE encoder (one window per group), `Some` while compressing.
174 flate: Option<moq_flate::Encoder>,
175
176 /// Bytes of deltas emitted into the current group, excluding the snapshot frame. Compressed
177 /// slice sizes when compressing, raw patch sizes otherwise.
178 delta_bytes: u64,
179
180 /// Reference size the delta budget is measured against: the current group's snapshot frame.
181 /// Its compressed slice size when compressing, raw otherwise.
182 snapshot_len: u64,
183
184 /// Frames emitted into the current group, snapshot included.
185 group_frames: usize,
186
187 /// Whether the next frame has to be a full snapshot, because a frame was lost or the caller cut
188 /// the group. Kept separate from [`last`](Self::last) so a resync doesn't erase the value: that
189 /// field is also what [`Producer::lock`](super::Producer::lock) seeds an edit from, and dropping
190 /// it there would publish a document with every other field missing.
191 resync: bool,
192
193 _marker: PhantomData<fn(T)>,
194}
195
196impl<T> Encoder<T> {
197 /// Create an encoder with a cold baseline, so the first [`update`](Self::update) is a snapshot.
198 pub fn new(config: ProducerConfig) -> Self {
199 Self {
200 config,
201 last: None,
202 flate: None,
203 delta_bytes: 0,
204 snapshot_len: 0,
205 group_frames: 0,
206 resync: false,
207 _marker: PhantomData,
208 }
209 }
210
211 /// The last encoded value, or `None` before the first snapshot.
212 ///
213 /// This is the baseline the next delta is diffed against, which is what a caller editing the
214 /// value in place needs to start from.
215 pub fn value(&self) -> Option<&Value> {
216 self.last.as_ref()
217 }
218
219 /// Force the next [`update`](Self::update) to emit a full snapshot, even for an unchanged value.
220 ///
221 /// Call this whenever the caller closes the current group behind the encoder's back (a
222 /// `cut`, a `seek`, a discontinuity). Without it the next value may be encoded as a delta
223 /// against a DEFLATE window and a baseline that the new group doesn't carry.
224 ///
225 /// [`value`](Self::value) survives: the snapshot republishes it in full anyway, and it is what a
226 /// caller editing in place starts from.
227 pub fn reset(&mut self) {
228 self.flate = None;
229 self.delta_bytes = 0;
230 self.snapshot_len = 0;
231 self.group_frames = 0;
232 self.resync = true;
233 }
234}
235
236impl<T: Serialize> Encoder<T> {
237 /// Encode a new value, as a snapshot or a delta.
238 ///
239 /// Returns `None` when the value is unchanged from the last one encoded, so nothing needs to be
240 /// written. Otherwise the frame comes back as a [`Pending`] the caller writes and then
241 /// [`commit`](Pending::commit)s; dropping it uncommitted resynchronizes the encoder.
242 pub fn update(&mut self, value: &T) -> Result<Option<Pending<'_, T>>> {
243 Ok(self.encode(value)?.map(|encoded| Pending {
244 encoder: self,
245 encoded,
246 committed: false,
247 }))
248 }
249
250 /// Encode a new value into a bare frame, advancing the encoder's state.
251 ///
252 /// The state change is what [`Pending`] guards, so this stays private: every caller goes through
253 /// [`update`](Self::update) and has to say whether the frame reached the wire.
254 fn encode(&mut self, value: &T) -> Result<Option<Encoded>> {
255 // A lost frame, or a group the caller cut, leaves the consumer's state unknown. Re-seed with a
256 // full snapshot even when the value is unchanged, since the frame that carried it may never
257 // have landed.
258 if self.resync {
259 return self.snapshot(value).map(Some);
260 }
261
262 // The first update has no baseline to diff against, so it seeds the stream with a snapshot.
263 let Some(last) = self.last.as_ref() else {
264 return self.snapshot(value).map(Some);
265 };
266
267 // Diff straight off `T`, without building a full `Value` for the new value first.
268 let Diff { patch, forced_snapshot } = diff(last, value);
269
270 // An empty object patch with no forced null means the value is unchanged: encode nothing.
271 if !forced_snapshot && patch.as_object().is_some_and(serde_json::Map::is_empty) {
272 return Ok(None);
273 }
274
275 // A forced snapshot (a genuine null, or a non-object root) or an exhausted delta budget starts a
276 // new group; otherwise the change rides as a delta in the open one.
277 if forced_snapshot || !self.delta_allowed() {
278 return self.snapshot(value).map(Some);
279 }
280
281 // Compress into the per-group window only now, for a frame we are committed to emitting.
282 let bytes = serde_json::to_vec(&patch)?;
283 let payload = match self.flate.as_mut() {
284 Some(flate) => flate.frame(&bytes),
285 None => Bytes::from(bytes),
286 };
287 self.delta_bytes += payload.len() as u64;
288 self.group_frames += 1;
289
290 // Fold the delta into the baseline so the next diff is against the value we just encoded.
291 json_patch::merge(self.last.as_mut().expect("a snapshot precedes any delta"), &patch);
292
293 Ok(Some(Encoded {
294 payload,
295 keyframe: false,
296 }))
297 }
298
299 /// Whether the current change may ride as a delta in the open group.
300 ///
301 /// The budget gate measures the deltas *already emitted* (excluding the frame about to land)
302 /// against the group's snapshot frame. Both are compressed sizes when compressing and raw
303 /// otherwise, so the comparison is like-for-like. Because the pending frame is excluded, the delta
304 /// that tips the group past `ratio * snapshot` still lands: a group overshoots by at most one delta
305 /// before rolling.
306 fn delta_allowed(&self) -> bool {
307 let ratio = u64::from(self.config.delta_ratio);
308 ratio != 0
309 && self.group_frames > 0
310 && self.group_frames < MAX_DELTA_FRAMES
311 && self.delta_bytes <= ratio * self.snapshot_len
312 }
313
314 /// Encode a full snapshot of `value`, opening a new group and reseeding the baseline.
315 fn snapshot(&mut self, value: &T) -> Result<Encoded> {
316 // Serialize directly from `value` so the snapshot frame preserves the type's own field order,
317 // keeping the wire bytes identical to serializing `T` straight to a frame.
318 let snapshot = serde_json::to_vec(value)?;
319
320 // Read the baseline back out of those same bytes rather than serializing `value` a second
321 // time, so the baseline IS the emitted snapshot by construction. A `Serialize` impl reading a
322 // clock or interior mutable state would otherwise seed the baseline with a value no consumer
323 // ever received, and every later delta would rebase them onto it. `T` is also only visited
324 // once, which is what a caller with an expensive or effectful `Serialize` pays for.
325 //
326 // This trades a second walk of `T` for a parse of the bytes, so it is not automatically
327 // cheaper than `to_value` (see the `baseline` benchmark); consistency is the reason.
328 //
329 // Both fallible steps run before any state changes, so a failure leaves the encoder exactly
330 // as it was rather than half-advanced with no frame to show for it.
331 let last = serde_json::from_slice(&snapshot)?;
332
333 // Open a fresh per-group encoder (cold window) and compress the snapshot as frame 0, recording
334 // its wire size as the delta anchor.
335 let (payload, flate) = match self.config.compression {
336 true => {
337 let mut flate = moq_flate::Encoder::new();
338 let payload = flate.frame(&snapshot);
339 (payload, Some(flate))
340 }
341 false => (Bytes::from(snapshot), None),
342 };
343
344 self.snapshot_len = payload.len() as u64;
345 self.delta_bytes = 0;
346 self.group_frames = 1;
347 self.flate = flate;
348 self.last = Some(last);
349 self.resync = false;
350
351 Ok(Encoded {
352 payload,
353 keyframe: true,
354 })
355 }
356}
357
358#[cfg(test)]
359mod test {
360 use super::*;
361 use serde_json::json;
362
363 /// Encode a sequence of values, committing each frame, and return `(keyframe, payload_len)` per
364 /// emitted frame.
365 fn encode(config: ProducerConfig, values: &[Value]) -> Vec<(bool, usize)> {
366 let mut encoder = Encoder::<Value>::new(config);
367 let mut out = Vec::new();
368 for value in values {
369 if let Some(frame) = encoder.update(value).unwrap() {
370 out.push((frame.keyframe, frame.payload.len()));
371 frame.commit();
372 }
373 }
374 out
375 }
376
377 /// Encode one value and commit it, returning the frame.
378 fn commit(encoder: &mut Encoder<Value>, value: &Value) -> Option<Encoded> {
379 let frame = encoder.update(value).unwrap()?;
380 let encoded = Encoded {
381 payload: frame.payload.clone(),
382 keyframe: frame.keyframe,
383 };
384 frame.commit();
385 Some(encoded)
386 }
387
388 #[test]
389 fn first_update_is_a_keyframe() {
390 let frames = encode(ProducerConfig::default(), &[json!({ "a": 1 })]);
391 assert_eq!(frames.len(), 1);
392 assert!(frames[0].0);
393 }
394
395 #[test]
396 fn unchanged_value_encodes_nothing() {
397 let frames = encode(ProducerConfig::default(), &[json!({ "a": 1 }), json!({ "a": 1 })]);
398 assert_eq!(frames.len(), 1);
399 }
400
401 #[test]
402 fn changes_ride_as_deltas() {
403 let frames = encode(
404 ProducerConfig::default().with_delta_ratio(100),
405 &[
406 json!({ "a": 1, "b": 1 }),
407 json!({ "a": 1, "b": 2 }),
408 json!({ "a": 1, "b": 3 }),
409 ],
410 );
411 assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, false, false]);
412 }
413
414 #[test]
415 fn deltas_off_forces_a_keyframe_per_change() {
416 let frames = encode(
417 ProducerConfig::default().with_delta_ratio(0),
418 &[json!({ "a": 1 }), json!({ "a": 2 })],
419 );
420 assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
421 }
422
423 /// A value the caller might reasonably expect to be a delta, but that merge patch can't express:
424 /// setting a field to JSON null reads as a key deletion. The encoder has to override the caller
425 /// here, which is why `keyframe` is a return value rather than a parameter.
426 #[test]
427 fn a_null_field_forces_a_keyframe() {
428 let frames = encode(
429 ProducerConfig::default().with_delta_ratio(100),
430 &[json!({ "a": 1, "b": 1 }), json!({ "a": 1, "b": null })],
431 );
432 assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
433 }
434
435 /// Same story for a root that isn't an object: there is no recursive merge patch for it.
436 #[test]
437 fn a_non_object_root_forces_a_keyframe() {
438 let frames = encode(
439 ProducerConfig::default().with_delta_ratio(100),
440 &[json!({ "a": 1 }), json!([1, 2, 3])],
441 );
442 assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
443 }
444
445 #[test]
446 fn frame_cap_forces_a_keyframe() {
447 let values: Vec<Value> = (0..=MAX_DELTA_FRAMES).map(|n| json!({ "n": n })).collect();
448 let frames = encode(ProducerConfig::default().with_delta_ratio(1_000_000), &values);
449
450 // The snapshot plus MAX_DELTA_FRAMES - 1 deltas fill the group, then the cap rolls it.
451 assert_eq!(frames.len(), MAX_DELTA_FRAMES + 1);
452 assert_eq!(frames.iter().filter(|f| f.0).count(), 2);
453 assert!(frames[MAX_DELTA_FRAMES].0);
454 }
455
456 /// A caller that cuts the group behind the encoder's back has to say so, or the next value would
457 /// be a delta against a window and a baseline the new group never carried.
458 #[test]
459 fn reset_forces_the_next_update_to_be_a_keyframe() {
460 let mut encoder = Encoder::<Value>::new(ProducerConfig::default().with_delta_ratio(100));
461 assert!(commit(&mut encoder, &json!({ "a": 1 })).unwrap().keyframe);
462 assert!(!commit(&mut encoder, &json!({ "a": 2 })).unwrap().keyframe);
463
464 encoder.reset();
465 assert!(commit(&mut encoder, &json!({ "a": 3 })).unwrap().keyframe);
466 }
467
468 /// A frame the caller never wrote must not leave the encoder emitting deltas against a baseline
469 /// no consumer received. Dropping the [`Pending`] uncommitted is what a failed write looks like,
470 /// and it has to resynchronize on its own: a caller cannot be relied on to remember.
471 #[test]
472 fn an_uncommitted_frame_resynchronizes_the_encoder() {
473 let mut encoder = Encoder::<Value>::new(ProducerConfig::default().with_delta_ratio(100));
474 commit(&mut encoder, &json!({ "a": 1 })).unwrap();
475
476 // The caller wrote this one and said so, so the next value can still ride as a delta.
477 commit(&mut encoder, &json!({ "a": 2 })).unwrap();
478
479 // This one fails to write, so the caller drops it without committing.
480 drop(encoder.update(&json!({ "a": 3 })).unwrap().expect("a delta"));
481
482 // The next value opens a new group with a full snapshot rather than patching a state the
483 // consumer never reached.
484 let recovered = commit(&mut encoder, &json!({ "a": 4 })).expect("a resynchronizing snapshot");
485 assert!(recovered.keyframe);
486 assert_eq!(
487 serde_json::from_slice::<Value>(&recovered.payload).unwrap(),
488 json!({ "a": 4 }),
489 "the snapshot carries the whole value, not a patch"
490 );
491 }
492
493 /// The same recovery when the very first frame is lost: the encoder must not treat the value as
494 /// already published and skip it as unchanged.
495 #[test]
496 fn an_uncommitted_first_frame_is_reencoded() {
497 let mut encoder = Encoder::<Value>::new(ProducerConfig::default());
498 drop(encoder.update(&json!({ "a": 1 })).unwrap().expect("a snapshot"));
499
500 let retried = commit(&mut encoder, &json!({ "a": 1 })).expect("the same value, re-encoded");
501 assert!(retried.keyframe);
502 }
503
504 /// A reset value is republished even when it matches the last one encoded: the new group has to
505 /// open with a snapshot, so "unchanged" can't mean "write nothing" there.
506 #[test]
507 fn reset_republishes_an_unchanged_value() {
508 let mut encoder = Encoder::<Value>::new(ProducerConfig::default());
509 commit(&mut encoder, &json!({ "a": 1 })).unwrap();
510
511 encoder.reset();
512 assert!(
513 commit(&mut encoder, &json!({ "a": 1 }))
514 .expect("a fresh snapshot")
515 .keyframe
516 );
517 }
518
519 #[test]
520 fn compressed_deltas_reuse_the_group_window() {
521 let phrase = "Media over QUIC delivers real-time latency at massive scale";
522 let frames = encode(
523 ProducerConfig::default().with_delta_ratio(100).with_compression(true),
524 &[json!({ "note": phrase }), json!({ "note": phrase, "echo": phrase })],
525 );
526
527 // The raw patch repeats the whole phrase; compressed against the window it's a fraction.
528 let raw = serde_json::to_vec(&json!({ "echo": phrase })).unwrap().len();
529 assert_eq!(frames.len(), 2);
530 assert!(
531 frames[1].1 < raw / 2,
532 "windowed delta {} vs raw patch {raw}",
533 frames[1].1
534 );
535 }
536
537 /// A value whose serialization changes on every call, standing in for a `Serialize` impl backed by
538 /// a clock, an atomic, or interior mutable state.
539 struct Ticking(std::cell::Cell<u32>);
540
541 impl serde::Serialize for Ticking {
542 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
543 use serde::ser::SerializeMap;
544
545 let n = self.0.get();
546 self.0.set(n + 1);
547
548 let mut map = serializer.serialize_map(Some(1))?;
549 map.serialize_entry("n", &n)?;
550 map.end()
551 }
552 }
553
554 /// The snapshot frame and the baseline must come from a single pass over the value. Serializing
555 /// twice costs a second traversal, and for a value like this one it seeds the baseline with
556 /// something no consumer ever received, so every later delta rebases them onto a phantom state.
557 #[test]
558 fn a_snapshot_serializes_its_value_once() {
559 let value = Ticking(std::cell::Cell::new(0));
560 let mut encoder = Encoder::<Ticking>::new(ProducerConfig::default());
561 let payload = {
562 let frame = encoder.update(&value).unwrap().expect("a snapshot");
563 let payload = frame.payload.clone();
564 frame.commit();
565 payload
566 };
567
568 assert_eq!(value.0.get(), 1, "the value should be serialized exactly once");
569
570 let emitted: Value = serde_json::from_slice(&payload).unwrap();
571 assert_eq!(emitted, json!({ "n": 0 }));
572 assert_eq!(encoder.value(), Some(&emitted), "the baseline must be what was emitted");
573 }
574
575 #[test]
576 fn value_tracks_the_baseline() {
577 let mut encoder = Encoder::<Value>::new(ProducerConfig::default().with_delta_ratio(100));
578 assert_eq!(encoder.value(), None);
579
580 commit(&mut encoder, &json!({ "a": 1, "b": 1 }));
581 commit(&mut encoder, &json!({ "a": 1, "b": 2 }));
582
583 // The delta was folded into the baseline, so it reflects what was actually published.
584 assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 2 })));
585 }
586}