1use std::collections::BTreeMap;
4
5use serde::de::{self, MapAccess, SeqAccess, Visitor};
6use serde::Deserialize;
7use serde_json::{Map, Number, Value};
8
9use crate::budget::BudgetUsageSnapshot;
10use crate::checkpoint::{
11 canonical_json_bytes, CheckpointError, CheckpointResult, CheckpointStatus,
12 CHECKPOINT_V2_SCHEMA, DEFAULT_MAX_EXTENSION_STATE_BYTES, RUN_DEFINITION_SCHEMA,
13};
14use crate::runtime::state::Checkpoint;
15use crate::runtime::state_v2::{
16 validate_checkpoint_v2, validate_extension_state_size, CheckpointV2, EventOutboxEntry,
17 ExtensionStateEntry, OperationJournalEntry,
18};
19use crate::types::{AgentStatus, CycleRecord, Message};
20
21const KNOWN_FIELDS: &[&str] = &[
22 "schema_version",
23 "run_definition_schema",
24 "run_definition",
25 "checkpoint_key",
26 "task_id",
27 "root_run_id",
28 "trace_id",
29 "run_definition_digest",
30 "resume_attempt",
31 "cycle_index",
32 "status",
33 "messages",
34 "cycles",
35 "shared_state",
36 "budget_usage",
37 "event_cursor",
38 "event_outbox",
39 "extension_state",
40 "model_call_journal",
41 "tool_journal",
42 "revision",
43 "claim_token",
44 "claimed_cycle",
45 "lease_expires_at_ms",
46 "terminal_result",
47 "terminal_acknowledged",
48];
49
50#[derive(Debug, Clone, PartialEq)]
51#[allow(clippy::large_enum_variant)] pub enum DecodedCheckpoint {
53 V1(Checkpoint),
54 V2(CheckpointV2),
55}
56
57pub fn checkpoint_v2_to_value(
58 checkpoint: &CheckpointV2,
59 max_extension_state_bytes: u64,
60) -> CheckpointResult<Value> {
61 validate_checkpoint_v2(checkpoint)?;
62 let extension_bytes =
63 validate_extension_state_size(&checkpoint.extension_state, max_extension_state_bytes);
64 extension_bytes?;
65
66 let mut object = checkpoint
67 .unknown_fields
68 .iter()
69 .map(|(key, value)| (key.clone(), value.clone()))
70 .collect::<Map<_, _>>();
71 object.insert(
72 "schema_version".to_string(),
73 Value::String(CHECKPOINT_V2_SCHEMA.to_string()),
74 );
75 object.insert(
76 "run_definition_schema".to_string(),
77 Value::String(RUN_DEFINITION_SCHEMA.to_string()),
78 );
79 object.insert(
80 "run_definition".to_string(),
81 checkpoint.run_definition.clone(),
82 );
83 object.insert(
84 "checkpoint_key".to_string(),
85 Value::String(checkpoint.checkpoint_key.clone()),
86 );
87 object.insert(
88 "task_id".to_string(),
89 Value::String(checkpoint.task_id.clone()),
90 );
91 object.insert(
92 "root_run_id".to_string(),
93 Value::String(checkpoint.root_run_id.clone()),
94 );
95 object.insert(
96 "trace_id".to_string(),
97 Value::String(checkpoint.trace_id.clone()),
98 );
99 object.insert(
100 "run_definition_digest".to_string(),
101 Value::String(checkpoint.run_definition_digest.clone()),
102 );
103 object.insert(
104 "resume_attempt".to_string(),
105 Value::from(checkpoint.resume_attempt),
106 );
107 object.insert(
108 "cycle_index".to_string(),
109 Value::from(checkpoint.cycle_index),
110 );
111 object.insert(
112 "status".to_string(),
113 Value::String(checkpoint.status.as_str().to_string()),
114 );
115 object.insert(
116 "messages".to_string(),
117 Value::Array(checkpoint.messages.iter().map(Message::to_dict).collect()),
118 );
119 object.insert(
120 "cycles".to_string(),
121 Value::Array(
122 checkpoint
123 .cycles
124 .iter()
125 .map(checkpoint_cycle_to_value)
126 .collect(),
127 ),
128 );
129 object.insert(
130 "shared_state".to_string(),
131 Value::Object(checkpoint.shared_state.clone().into_iter().collect()),
132 );
133 object.insert(
134 "budget_usage".to_string(),
135 checkpoint
136 .budget_usage
137 .as_ref()
138 .map(serde_json::to_value)
139 .transpose()
140 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?
141 .unwrap_or(Value::Null),
142 );
143 object.insert(
144 "event_cursor".to_string(),
145 checkpoint
146 .event_cursor
147 .as_ref()
148 .map(serde_json::to_value)
149 .transpose()
150 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?
151 .unwrap_or(Value::Null),
152 );
153 object.insert(
154 "event_outbox".to_string(),
155 Value::Array(
156 checkpoint
157 .event_outbox
158 .iter()
159 .map(EventOutboxEntry::to_value)
160 .collect(),
161 ),
162 );
163 object.insert(
164 "extension_state".to_string(),
165 Value::Object(
166 checkpoint
167 .extension_state
168 .iter()
169 .map(|(namespace, entry)| (namespace.clone(), entry.to_value()))
170 .collect(),
171 ),
172 );
173 object.insert(
174 "model_call_journal".to_string(),
175 Value::Array(
176 checkpoint
177 .model_call_journal
178 .iter()
179 .map(OperationJournalEntry::to_value)
180 .collect(),
181 ),
182 );
183 object.insert(
184 "tool_journal".to_string(),
185 Value::Array(
186 checkpoint
187 .tool_journal
188 .iter()
189 .map(OperationJournalEntry::to_value)
190 .collect(),
191 ),
192 );
193 object.insert("revision".to_string(), Value::from(checkpoint.revision));
194 object.insert(
195 "claim_token".to_string(),
196 checkpoint
197 .claim_token
198 .clone()
199 .map_or(Value::Null, Value::String),
200 );
201 object.insert(
202 "claimed_cycle".to_string(),
203 checkpoint.claimed_cycle.map_or(Value::Null, Value::from),
204 );
205 object.insert(
206 "lease_expires_at_ms".to_string(),
207 checkpoint
208 .lease_expires_at_ms
209 .map_or(Value::Null, Value::from),
210 );
211 object.insert(
212 "terminal_result".to_string(),
213 checkpoint.terminal_result.clone().unwrap_or(Value::Null),
214 );
215 object.insert(
216 "terminal_acknowledged".to_string(),
217 Value::Bool(checkpoint.terminal_acknowledged),
218 );
219 Ok(Value::Object(object))
220}
221
222pub fn checkpoint_v2_from_value(
223 payload: &Value,
224 max_extension_state_bytes: u64,
225) -> CheckpointResult<CheckpointV2> {
226 let object = payload.as_object().ok_or_else(|| {
227 CheckpointError::new(
228 "checkpoint_payload_invalid",
229 "checkpoint v2 payload must be an object",
230 )
231 })?;
232 if object.get("schema_version").and_then(Value::as_str) != Some(CHECKPOINT_V2_SCHEMA) {
233 return Err(CheckpointError::new(
234 "checkpoint_schema_unsupported",
235 "checkpoint schema_version is not vv-agent.checkpoint.v2",
236 ));
237 }
238 let run_definition_schema = required_string(
239 object,
240 "run_definition_schema",
241 "checkpoint_definition_schema_unsupported",
242 )?;
243 if run_definition_schema != RUN_DEFINITION_SCHEMA {
244 return Err(CheckpointError::new(
245 "checkpoint_definition_schema_unsupported",
246 "run_definition_schema is unsupported",
247 ));
248 }
249 let run_definition = object.get("run_definition").cloned().ok_or_else(|| {
250 CheckpointError::new(
251 "checkpoint_definition_invalid",
252 "run_definition is required",
253 )
254 })?;
255 if let Some(field) = KNOWN_FIELDS
256 .iter()
257 .find(|field| !object.contains_key(**field))
258 {
259 return Err(CheckpointError::new(
260 "checkpoint_field_invalid",
261 format!("required checkpoint field {field} is missing"),
262 ));
263 }
264 let checkpoint = CheckpointV2 {
265 schema_version: CHECKPOINT_V2_SCHEMA.to_string(),
266 run_definition_schema: run_definition_schema.to_string(),
267 run_definition,
268 checkpoint_key: required_string(object, "checkpoint_key", "checkpoint_key_invalid")?
269 .to_string(),
270 task_id: required_string(object, "task_id", "checkpoint_value_invalid")?.to_string(),
271 root_run_id: required_string(object, "root_run_id", "checkpoint_value_invalid")?
272 .to_string(),
273 trace_id: required_string(object, "trace_id", "checkpoint_value_invalid")?.to_string(),
274 run_definition_digest: required_string(
275 object,
276 "run_definition_digest",
277 "checkpoint_digest_invalid",
278 )?
279 .to_string(),
280 resume_attempt: required_u64(
281 object,
282 "resume_attempt",
283 "checkpoint_resume_attempt_invalid",
284 )?,
285 cycle_index: required_u64(object, "cycle_index", "checkpoint_cycle_invalid")?,
286 status: parse_status(object, "status")?,
287 messages: parse_messages(object.get("messages"))?,
288 cycles: parse_cycles(object.get("cycles"))?,
289 shared_state: parse_object_map(object, "shared_state", "checkpoint_shared_state_invalid")?,
290 budget_usage: parse_optional_budget(object.get("budget_usage"))?,
291 event_cursor: parse_optional(object.get("event_cursor"), "event_cursor")?,
292 event_outbox: parse_array(object, "event_outbox")?
293 .iter()
294 .map(EventOutboxEntry::from_value)
295 .collect::<CheckpointResult<Vec<_>>>()?,
296 extension_state: parse_extensions(object.get("extension_state"))?,
297 model_call_journal: parse_array(object, "model_call_journal")?
298 .iter()
299 .map(OperationJournalEntry::from_value)
300 .collect::<CheckpointResult<Vec<_>>>()?,
301 tool_journal: parse_array(object, "tool_journal")?
302 .iter()
303 .map(OperationJournalEntry::from_value)
304 .collect::<CheckpointResult<Vec<_>>>()?,
305 revision: required_u64(object, "revision", "checkpoint_revision_invalid")?,
306 claim_token: parse_optional_string(object, "claim_token")?,
307 claimed_cycle: parse_optional_u64(object, "claimed_cycle")?,
308 lease_expires_at_ms: parse_optional_u64(object, "lease_expires_at_ms")?,
309 terminal_result: parse_optional_value(object, "terminal_result")?,
310 terminal_acknowledged: object
311 .get("terminal_acknowledged")
312 .and_then(Value::as_bool)
313 .ok_or_else(|| {
314 CheckpointError::new(
315 "checkpoint_status_invalid",
316 "terminal_acknowledged must be a boolean",
317 )
318 })?,
319 unknown_fields: object
320 .iter()
321 .filter(|(key, _)| !KNOWN_FIELDS.contains(&key.as_str()))
322 .map(|(key, value)| (key.clone(), value.clone()))
323 .collect(),
324 };
325 validate_checkpoint_v2(&checkpoint)?;
326 crate::runtime::state_v2::validate_extension_state_size(
327 &checkpoint.extension_state,
328 max_extension_state_bytes,
329 )?;
330 Ok(checkpoint)
331}
332
333pub fn checkpoint_v2_to_json(
334 checkpoint: &CheckpointV2,
335 max_extension_state_bytes: u64,
336) -> CheckpointResult<String> {
337 let value = checkpoint_v2_to_value(checkpoint, max_extension_state_bytes)?;
338 let bytes = canonical_json_bytes(&value, "checkpoint v2")?;
339 String::from_utf8(bytes).map_err(|error| {
340 CheckpointError::new(
341 "checkpoint_canonicalization_invalid",
342 format!("checkpoint canonical JSON is not UTF-8: {error}"),
343 )
344 })
345}
346
347pub fn checkpoint_v2_from_json(
348 payload: &str,
349 max_extension_state_bytes: u64,
350) -> CheckpointResult<CheckpointV2> {
351 let value = strict_json_value(payload)?;
352 checkpoint_v2_from_value(&value, max_extension_state_bytes)
353}
354
355pub fn decode_checkpoint(payload: &str) -> CheckpointResult<DecodedCheckpoint> {
356 let value = strict_json_value(payload)?;
357 let object = value.as_object().ok_or_else(|| {
358 CheckpointError::new(
359 "checkpoint_payload_invalid",
360 "checkpoint payload must be an object",
361 )
362 })?;
363 match object.get("schema_version") {
364 None => crate::runtime::checkpoint_codec::checkpoint_from_json(payload)
365 .map(DecodedCheckpoint::V1)
366 .map_err(|error| CheckpointError::new("checkpoint_v1_invalid", error.to_string())),
367 Some(Value::String(schema)) if schema == CHECKPOINT_V2_SCHEMA => {
368 checkpoint_v2_from_value(&value, DEFAULT_MAX_EXTENSION_STATE_BYTES)
369 .map(DecodedCheckpoint::V2)
370 }
371 Some(_) => Err(CheckpointError::new(
372 "checkpoint_schema_unsupported",
373 "a present checkpoint schema discriminator is unsupported",
374 )),
375 }
376}
377
378pub fn decode_checkpoint_bytes(payload: &[u8]) -> CheckpointResult<DecodedCheckpoint> {
379 let payload = std::str::from_utf8(payload).map_err(|error| {
380 CheckpointError::new(
381 "checkpoint_json_invalid",
382 format!("checkpoint is not UTF-8: {error}"),
383 )
384 })?;
385 decode_checkpoint(payload)
386}
387
388pub fn encode_checkpoint_v1(checkpoint: &Checkpoint) -> CheckpointResult<String> {
389 crate::runtime::checkpoint_codec::checkpoint_to_json(checkpoint)
390 .map_err(|error| CheckpointError::new("checkpoint_v1_invalid", error.to_string()))
391}
392
393pub fn migrate_terminal_v1(
396 checkpoint: &Checkpoint,
397 checkpoint_key: impl Into<String>,
398 root_run_id: impl Into<String>,
399 trace_id: impl Into<String>,
400 run_definition: Value,
401) -> CheckpointResult<CheckpointV2> {
402 if checkpoint.claim_token.is_some()
403 || checkpoint.claimed_cycle.is_some()
404 || checkpoint.lease_expires_at_ms.is_some()
405 {
406 return Err(CheckpointError::new(
407 "checkpoint_migration_active_claim",
408 "an active v1 claim cannot be migrated",
409 ));
410 }
411 let Some(terminal_result) = checkpoint.terminal_result.as_ref() else {
412 return Err(CheckpointError::new(
413 "checkpoint_migration_requires_reconciliation",
414 "only a durable v1 terminal can be migrated automatically",
415 ));
416 };
417 let status = checkpoint_status_from_v1(checkpoint.status)?;
418 let digest = crate::checkpoint::run_definition_digest(&run_definition)?;
419 let terminal_result = serde_json::to_value(terminal_result)
420 .map_err(|error| CheckpointError::new("checkpoint_migration_invalid", error.to_string()))?;
421 let migrated = CheckpointV2 {
422 schema_version: CHECKPOINT_V2_SCHEMA.to_string(),
423 run_definition_schema: RUN_DEFINITION_SCHEMA.to_string(),
424 run_definition,
425 checkpoint_key: checkpoint_key.into(),
426 task_id: checkpoint.task_id.clone(),
427 root_run_id: root_run_id.into(),
428 trace_id: trace_id.into(),
429 run_definition_digest: digest,
430 resume_attempt: 1,
431 cycle_index: u64::from(checkpoint.cycle_index),
432 status,
433 messages: checkpoint.messages.clone(),
434 cycles: checkpoint.cycles.clone(),
435 shared_state: checkpoint.shared_state.clone(),
436 budget_usage: checkpoint.budget_usage.clone(),
437 event_cursor: None,
438 event_outbox: Vec::new(),
439 extension_state: BTreeMap::new(),
440 model_call_journal: Vec::new(),
441 tool_journal: Vec::new(),
442 revision: checkpoint.revision,
443 claim_token: None,
444 claimed_cycle: None,
445 lease_expires_at_ms: None,
446 terminal_result: Some(terminal_result),
447 terminal_acknowledged: false,
448 unknown_fields: BTreeMap::new(),
449 };
450 migrated.validate()?;
451 Ok(migrated)
452}
453
454fn checkpoint_status_from_v1(status: AgentStatus) -> CheckpointResult<CheckpointStatus> {
455 match status {
456 AgentStatus::Completed => Ok(CheckpointStatus::Completed),
457 AgentStatus::Failed => Ok(CheckpointStatus::Failed),
458 AgentStatus::MaxCycles => Ok(CheckpointStatus::MaxCycles),
459 _ => Err(CheckpointError::new(
460 "checkpoint_migration_requires_reconciliation",
461 "v1 status is not a durable terminal",
462 )),
463 }
464}
465
466fn parse_status(object: &Map<String, Value>, field: &str) -> CheckpointResult<CheckpointStatus> {
467 let value = required_string(object, field, "checkpoint_status_invalid")?;
468 serde_json::from_value(Value::String(value.to_string())).map_err(|_| {
469 CheckpointError::new(
470 "checkpoint_status_invalid",
471 format!("unknown checkpoint status {value}"),
472 )
473 })
474}
475
476fn parse_messages(value: Option<&Value>) -> CheckpointResult<Vec<Message>> {
477 let values = value.and_then(Value::as_array).ok_or_else(|| {
478 CheckpointError::new("checkpoint_messages_invalid", "messages must be an array")
479 })?;
480 values
481 .iter()
482 .map(|value| {
483 Message::from_dict(value)
484 .map_err(|error| CheckpointError::new("checkpoint_messages_invalid", error))
485 })
486 .collect()
487}
488
489fn parse_cycles(value: Option<&Value>) -> CheckpointResult<Vec<CycleRecord>> {
490 let values = value.and_then(Value::as_array).ok_or_else(|| {
491 CheckpointError::new("checkpoint_cycles_invalid", "cycles must be an array")
492 })?;
493 values
494 .iter()
495 .map(|value| {
496 CycleRecord::from_dict(value)
497 .map_err(|error| CheckpointError::new("checkpoint_cycles_invalid", error))
498 })
499 .collect()
500}
501
502fn checkpoint_cycle_to_value(cycle: &CycleRecord) -> Value {
503 let mut value = cycle.to_dict();
504 if let Some(token_usage) = value
505 .as_object_mut()
506 .and_then(|cycle| cycle.get_mut("token_usage"))
507 .and_then(Value::as_object_mut)
508 {
509 if token_usage
510 .get("raw")
511 .is_some_and(|raw| raw.as_object().is_some_and(serde_json::Map::is_empty))
512 {
513 token_usage.remove("raw");
514 }
515 }
516 value
517}
518
519fn parse_object_map(
520 object: &Map<String, Value>,
521 field: &str,
522 code: &str,
523) -> CheckpointResult<BTreeMap<String, Value>> {
524 object
525 .get(field)
526 .and_then(Value::as_object)
527 .cloned()
528 .map(|map| map.into_iter().collect())
529 .ok_or_else(|| CheckpointError::new(code, format!("{field} must be an object")))
530}
531
532fn parse_optional_budget(value: Option<&Value>) -> CheckpointResult<Option<BudgetUsageSnapshot>> {
533 match value {
534 None | Some(Value::Null) => Ok(None),
535 Some(value) => serde_json::from_value(value.clone())
536 .map(Some)
537 .map_err(|error| {
538 CheckpointError::new("checkpoint_budget_usage_invalid", error.to_string())
539 }),
540 }
541}
542
543fn parse_optional<T>(value: Option<&Value>, field: &str) -> CheckpointResult<Option<T>>
544where
545 T: serde::de::DeserializeOwned,
546{
547 match value {
548 None | Some(Value::Null) => Ok(None),
549 Some(value) => serde_json::from_value(value.clone())
550 .map(Some)
551 .map_err(|error| {
552 CheckpointError::new("checkpoint_field_invalid", format!("{field}: {error}"))
553 }),
554 }
555}
556
557fn parse_extensions(
558 value: Option<&Value>,
559) -> CheckpointResult<BTreeMap<String, ExtensionStateEntry>> {
560 let object = value.and_then(Value::as_object).ok_or_else(|| {
561 CheckpointError::new(
562 "checkpoint_extension_state_invalid",
563 "extension_state must be an object",
564 )
565 })?;
566 object
567 .iter()
568 .map(|(namespace, value)| Ok((namespace.clone(), ExtensionStateEntry::from_value(value)?)))
569 .collect()
570}
571
572fn parse_optional_string(
573 object: &Map<String, Value>,
574 field: &str,
575) -> CheckpointResult<Option<String>> {
576 match object.get(field) {
577 None | Some(Value::Null) => Ok(None),
578 Some(Value::String(value)) => Ok(Some(value.clone())),
579 Some(_) => Err(CheckpointError::new(
580 "checkpoint_claim_invalid",
581 format!("{field} must be a string or null"),
582 )),
583 }
584}
585
586fn parse_optional_u64(object: &Map<String, Value>, field: &str) -> CheckpointResult<Option<u64>> {
587 match object.get(field) {
588 None | Some(Value::Null) => Ok(None),
589 Some(Value::Number(number)) => number.as_u64().map(Some).ok_or_else(|| {
590 CheckpointError::new(
591 "checkpoint_integer_invalid",
592 format!("{field} must be a non-negative integer"),
593 )
594 }),
595 Some(_) => Err(CheckpointError::new(
596 "checkpoint_integer_invalid",
597 format!("{field} must be an integer or null"),
598 )),
599 }
600}
601
602fn parse_optional_value(
603 object: &Map<String, Value>,
604 field: &str,
605) -> CheckpointResult<Option<Value>> {
606 Ok(object.get(field).filter(|value| !value.is_null()).cloned())
607}
608
609fn parse_array<'a>(
610 object: &'a Map<String, Value>,
611 field: &str,
612) -> CheckpointResult<&'a Vec<Value>> {
613 object.get(field).and_then(Value::as_array).ok_or_else(|| {
614 CheckpointError::new(
615 "checkpoint_field_invalid",
616 format!("{field} must be an array"),
617 )
618 })
619}
620
621fn required_string<'a>(
622 object: &'a Map<String, Value>,
623 field: &str,
624 code: &str,
625) -> CheckpointResult<&'a str> {
626 object
627 .get(field)
628 .and_then(Value::as_str)
629 .ok_or_else(|| CheckpointError::new(code, format!("{field} must be a string")))
630}
631
632fn required_u64(object: &Map<String, Value>, field: &str, code: &str) -> CheckpointResult<u64> {
633 object.get(field).and_then(Value::as_u64).ok_or_else(|| {
634 CheckpointError::new(code, format!("{field} must be a non-negative integer"))
635 })
636}
637
638fn strict_json_value(payload: &str) -> CheckpointResult<Value> {
639 let mut deserializer = serde_json::Deserializer::from_str(payload);
640 let StrictValue(value) = StrictValue::deserialize(&mut deserializer)
641 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?;
642 deserializer
643 .end()
644 .map_err(|error| CheckpointError::new("checkpoint_json_invalid", error.to_string()))?;
645 Ok(value)
646}
647
648struct StrictValue(Value);
649
650impl<'de> Deserialize<'de> for StrictValue {
651 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
652 where
653 D: serde::Deserializer<'de>,
654 {
655 deserializer.deserialize_any(StrictValueVisitor)
656 }
657}
658
659struct StrictValueVisitor;
660
661impl<'de> Visitor<'de> for StrictValueVisitor {
662 type Value = StrictValue;
663
664 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 formatter.write_str("a JSON value without duplicate object keys")
666 }
667
668 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
669 Ok(StrictValue(Value::Bool(value)))
670 }
671
672 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
673 Ok(StrictValue(Value::Number(Number::from(value))))
674 }
675
676 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
677 Ok(StrictValue(Value::Number(Number::from(value))))
678 }
679
680 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
681 where
682 E: de::Error,
683 {
684 Number::from_f64(value)
685 .map(Value::Number)
686 .map(StrictValue)
687 .ok_or_else(|| E::custom("non-finite JSON number"))
688 }
689
690 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
691 Ok(StrictValue(Value::String(value.to_string())))
692 }
693
694 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
695 Ok(StrictValue(Value::String(value)))
696 }
697
698 fn visit_none<E>(self) -> Result<Self::Value, E> {
699 Ok(StrictValue(Value::Null))
700 }
701
702 fn visit_unit<E>(self) -> Result<Self::Value, E> {
703 Ok(StrictValue(Value::Null))
704 }
705
706 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
707 where
708 A: SeqAccess<'de>,
709 {
710 let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
711 while let Some(StrictValue(value)) = sequence.next_element()? {
712 values.push(value);
713 }
714 Ok(StrictValue(Value::Array(values)))
715 }
716
717 fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error>
718 where
719 A: MapAccess<'de>,
720 {
721 let mut values = Map::new();
722 while let Some(key) = object.next_key::<String>()? {
723 if values.contains_key(&key) {
724 return Err(de::Error::custom(format!(
725 "duplicate JSON object key {key}"
726 )));
727 }
728 let StrictValue(value) = object.next_value()?;
729 values.insert(key, value);
730 }
731 Ok(StrictValue(Value::Object(values)))
732 }
733}