Skip to main content

vv_agent/runtime/
checkpoint_codec_v2.rs

1//! Strict checkpoint v1/v2 decoding and the v2 whole-object codec.
2
3use 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)] // Public variants retain direct checkpoint values.
52pub 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
393/// Explicitly migrate a durable v1 terminal into a v2 record. Non-terminal
394/// and actively claimed v1 records are rejected rather than guessed into v2.
395pub 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}