Skip to main content

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}