1use std::str::FromStr;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::{Map, Value, json};
5use thiserror::Error;
6
7use crate::content::validate_tool_name;
8use crate::envelope::{deserialize_unique_value, validate_read_version};
9use crate::{
10 ApprovalDecision, ApprovalId, BranchId, CURRENT_PROTOCOL_VERSION, CanonicalMessage,
11 CausationId, ContentBlock, CorrelationId, ProfileId, ProtocolError, ProtocolMetadata,
12 ProtocolTimestamp, ProtocolVersion, RecordId, RunId, SessionId, SessionSequence, ToolCallId,
13 ToolFailure, ToolPresentation, TurnId,
14};
15
16pub const MAX_RECORD_CONTENT_BLOCKS: usize = 256;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum SessionRecordType {
23 SessionCreated,
25 MessageCommitted,
27 ConfigurationChanged,
29 ToolCallRequested,
31 PolicyDecisionRecorded,
33 ApprovalRequested,
35 ApprovalResolved,
37 ToolExecutionStarted,
39 ToolExecutionFinished,
41 ToolExecutionInterrupted,
43 RunInterrupted,
45 RunCancelled,
47 BranchCreated,
49 ActiveBranchChanged,
51 SessionCompacted,
53 TurnCheckpointed,
55}
56
57impl SessionRecordType {
58 pub const ALL: [Self; 16] = [
60 Self::SessionCreated,
61 Self::MessageCommitted,
62 Self::ConfigurationChanged,
63 Self::ToolCallRequested,
64 Self::PolicyDecisionRecorded,
65 Self::ApprovalRequested,
66 Self::ApprovalResolved,
67 Self::ToolExecutionStarted,
68 Self::ToolExecutionFinished,
69 Self::ToolExecutionInterrupted,
70 Self::RunInterrupted,
71 Self::RunCancelled,
72 Self::BranchCreated,
73 Self::ActiveBranchChanged,
74 Self::SessionCompacted,
75 Self::TurnCheckpointed,
76 ];
77
78 #[must_use]
80 pub const fn is_required_for_replay(self) -> bool {
81 true
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum PolicyDecision {
89 Allow,
91 Deny,
93 RequireApproval,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum ExecutionTarget {
101 Native,
103 Mcp,
105 Remote,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum ToolIdempotency {
113 Idempotent,
115 NonIdempotent,
117 ExternallyReconciled,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum NextTurnAction {
125 ModelRequest,
127 WaitForApproval,
129 FinishRun,
131}
132
133#[derive(Debug, Clone, PartialEq)]
135pub enum SessionRecord {
136 SessionCreated {
138 profile_id: ProfileId,
140 metadata: ProtocolMetadata,
142 },
143 MessageCommitted {
145 message: CanonicalMessage,
147 },
148 ConfigurationChanged {
150 model: Option<crate::ModelRef>,
152 profile_id: Option<ProfileId>,
154 reasoning_effort: Option<crate::ReasoningEffort>,
156 },
157 ToolCallRequested {
159 tool_call_id: ToolCallId,
161 tool_name: String,
163 arguments: Value,
165 },
166 PolicyDecisionRecorded {
168 tool_call_id: ToolCallId,
170 decision: PolicyDecision,
172 },
173 ApprovalRequested {
175 approval_id: ApprovalId,
177 tool_call_id: ToolCallId,
179 expires_at: ProtocolTimestamp,
181 },
182 ApprovalResolved {
184 approval_id: ApprovalId,
186 decision: ApprovalDecision,
188 },
189 ToolExecutionStarted {
191 tool_call_id: ToolCallId,
193 execution_target: ExecutionTarget,
195 idempotency: ToolIdempotency,
197 },
198 ToolExecutionFinished {
200 tool_call_id: ToolCallId,
202 is_error: bool,
204 content: Vec<ContentBlock>,
206 error: Option<ToolFailure>,
208 presentation: Option<ToolPresentation>,
210 },
211 ToolExecutionInterrupted {
213 tool_call_id: ToolCallId,
215 reason: String,
217 },
218 RunInterrupted {
220 run_id: RunId,
222 turn_id: TurnId,
224 reason: String,
226 },
227 RunCancelled {
229 run_id: RunId,
231 },
232 BranchCreated {
234 source_branch_id: BranchId,
236 branch_id: BranchId,
238 from_record_id: RecordId,
240 },
241 ActiveBranchChanged {
243 branch_id: BranchId,
245 },
246 SessionCompacted {
248 summary: CanonicalMessage,
250 compacted_through_record_id: RecordId,
252 },
253 TurnCheckpointed {
255 run_id: RunId,
257 turn_id: TurnId,
259 next_action: NextTurnAction,
261 },
262}
263
264impl SessionRecord {
265 #[must_use]
267 pub const fn record_type(&self) -> SessionRecordType {
268 match self {
269 Self::SessionCreated { .. } => SessionRecordType::SessionCreated,
270 Self::MessageCommitted { .. } => SessionRecordType::MessageCommitted,
271 Self::ConfigurationChanged { .. } => SessionRecordType::ConfigurationChanged,
272 Self::ToolCallRequested { .. } => SessionRecordType::ToolCallRequested,
273 Self::PolicyDecisionRecorded { .. } => SessionRecordType::PolicyDecisionRecorded,
274 Self::ApprovalRequested { .. } => SessionRecordType::ApprovalRequested,
275 Self::ApprovalResolved { .. } => SessionRecordType::ApprovalResolved,
276 Self::ToolExecutionStarted { .. } => SessionRecordType::ToolExecutionStarted,
277 Self::ToolExecutionFinished { .. } => SessionRecordType::ToolExecutionFinished,
278 Self::ToolExecutionInterrupted { .. } => SessionRecordType::ToolExecutionInterrupted,
279 Self::RunInterrupted { .. } => SessionRecordType::RunInterrupted,
280 Self::RunCancelled { .. } => SessionRecordType::RunCancelled,
281 Self::BranchCreated { .. } => SessionRecordType::BranchCreated,
282 Self::ActiveBranchChanged { .. } => SessionRecordType::ActiveBranchChanged,
283 Self::SessionCompacted { .. } => SessionRecordType::SessionCompacted,
284 Self::TurnCheckpointed { .. } => SessionRecordType::TurnCheckpointed,
285 }
286 }
287
288 fn validate(&self) -> Result<(), RecordValidationError> {
289 match self {
290 Self::ConfigurationChanged {
291 model,
292 profile_id,
293 reasoning_effort,
294 } if model.is_none() && profile_id.is_none() && reasoning_effort.is_none() => {
295 Err(RecordValidationError::EmptyConfigurationChange)
296 }
297 Self::ToolCallRequested {
298 tool_name,
299 arguments,
300 ..
301 } => {
302 validate_tool_name(tool_name)?;
303 if !arguments.is_object() {
304 return Err(RecordValidationError::ToolArgumentsMustBeObject);
305 }
306 crate::metadata::validate_json_bounds(
307 arguments,
308 crate::MAX_TOOL_ARGUMENT_BYTES,
309 crate::MAX_TOOL_ARGUMENT_DEPTH,
310 )?;
311 Ok(())
312 }
313 Self::ToolExecutionFinished {
314 is_error,
315 content,
316 error,
317 presentation,
318 ..
319 } => {
320 validate_result_content(content)?;
321 if *is_error != error.is_some() {
322 return Err(RecordValidationError::InconsistentToolFailure);
323 }
324 if *is_error && presentation.is_some() {
325 return Err(RecordValidationError::PresentationOnFailure);
326 }
327 Ok(())
328 }
329 Self::ToolExecutionInterrupted { reason, .. } | Self::RunInterrupted { reason, .. } => {
330 validate_reason(reason)
331 }
332 Self::SessionCreated { .. }
333 | Self::MessageCommitted { .. }
334 | Self::ConfigurationChanged { .. }
335 | Self::PolicyDecisionRecorded { .. }
336 | Self::ApprovalRequested { .. }
337 | Self::ApprovalResolved { .. }
338 | Self::ToolExecutionStarted { .. }
339 | Self::RunCancelled { .. }
340 | Self::BranchCreated { .. }
341 | Self::ActiveBranchChanged { .. }
342 | Self::SessionCompacted { .. }
343 | Self::TurnCheckpointed { .. } => Ok(()),
344 }
345 }
346}
347
348#[derive(Serialize, Deserialize)]
349#[serde(
350 remote = "SessionRecord",
351 tag = "type",
352 content = "payload",
353 rename_all = "snake_case"
354)]
355enum SessionRecordDef {
356 SessionCreated {
357 #[serde(rename = "profileId")]
358 profile_id: ProfileId,
359 #[serde(default, skip_serializing_if = "ProtocolMetadata::is_empty")]
360 metadata: ProtocolMetadata,
361 },
362 MessageCommitted {
363 message: CanonicalMessage,
364 },
365 ConfigurationChanged {
366 #[serde(skip_serializing_if = "Option::is_none")]
367 model: Option<crate::ModelRef>,
368 #[serde(rename = "profileId", skip_serializing_if = "Option::is_none")]
369 profile_id: Option<ProfileId>,
370 #[serde(
371 rename = "reasoningEffort",
372 default,
373 skip_serializing_if = "Option::is_none"
374 )]
375 reasoning_effort: Option<crate::ReasoningEffort>,
376 },
377 ToolCallRequested {
378 #[serde(rename = "toolCallId")]
379 tool_call_id: ToolCallId,
380 #[serde(rename = "toolName")]
381 tool_name: String,
382 arguments: Value,
383 },
384 PolicyDecisionRecorded {
385 #[serde(rename = "toolCallId")]
386 tool_call_id: ToolCallId,
387 decision: PolicyDecision,
388 },
389 ApprovalRequested {
390 #[serde(rename = "approvalId")]
391 approval_id: ApprovalId,
392 #[serde(rename = "toolCallId")]
393 tool_call_id: ToolCallId,
394 #[serde(rename = "expiresAt")]
395 expires_at: ProtocolTimestamp,
396 },
397 ApprovalResolved {
398 #[serde(rename = "approvalId")]
399 approval_id: ApprovalId,
400 decision: ApprovalDecision,
401 },
402 ToolExecutionStarted {
403 #[serde(rename = "toolCallId")]
404 tool_call_id: ToolCallId,
405 #[serde(rename = "executionTarget")]
406 execution_target: ExecutionTarget,
407 idempotency: ToolIdempotency,
408 },
409 ToolExecutionFinished {
410 #[serde(rename = "toolCallId")]
411 tool_call_id: ToolCallId,
412 #[serde(rename = "isError")]
413 is_error: bool,
414 content: Vec<ContentBlock>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 error: Option<ToolFailure>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 presentation: Option<ToolPresentation>,
419 },
420 ToolExecutionInterrupted {
421 #[serde(rename = "toolCallId")]
422 tool_call_id: ToolCallId,
423 reason: String,
424 },
425 RunInterrupted {
426 #[serde(rename = "runId")]
427 run_id: RunId,
428 #[serde(rename = "turnId")]
429 turn_id: TurnId,
430 reason: String,
431 },
432 RunCancelled {
433 #[serde(rename = "runId")]
434 run_id: RunId,
435 },
436 BranchCreated {
437 #[serde(rename = "sourceBranchId")]
438 source_branch_id: BranchId,
439 #[serde(rename = "branchId")]
440 branch_id: BranchId,
441 #[serde(rename = "fromRecordId")]
442 from_record_id: RecordId,
443 },
444 ActiveBranchChanged {
445 #[serde(rename = "branchId")]
446 branch_id: BranchId,
447 },
448 SessionCompacted {
449 summary: CanonicalMessage,
450 #[serde(rename = "compactedThroughRecordId")]
451 compacted_through_record_id: RecordId,
452 },
453 TurnCheckpointed {
454 #[serde(rename = "runId")]
455 run_id: RunId,
456 #[serde(rename = "turnId")]
457 turn_id: TurnId,
458 #[serde(rename = "nextAction")]
459 next_action: NextTurnAction,
460 },
461}
462
463impl Serialize for SessionRecord {
464 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
465 where
466 S: Serializer,
467 {
468 self.validate().map_err(serde::ser::Error::custom)?;
469 SessionRecordDef::serialize(self, serializer)
470 }
471}
472
473impl<'de> Deserialize<'de> for SessionRecord {
474 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
475 where
476 D: Deserializer<'de>,
477 {
478 let record = SessionRecordDef::deserialize(deserializer)?;
479 record.validate().map_err(serde::de::Error::custom)?;
480 Ok(record)
481 }
482}
483
484#[derive(Debug, Clone, PartialEq)]
486pub struct RecordEnvelope {
487 protocol_version: ProtocolVersion,
488 record_id: RecordId,
489 session_id: SessionId,
490 sequence: SessionSequence,
491 timestamp: ProtocolTimestamp,
492 causation_id: Option<CausationId>,
493 correlation_id: Option<CorrelationId>,
494 branch_id: Option<BranchId>,
495 metadata: ProtocolMetadata,
496 record: SessionRecord,
497}
498
499impl RecordEnvelope {
500 #[allow(clippy::too_many_arguments)]
506 pub fn new(
507 record_id: RecordId,
508 session_id: SessionId,
509 sequence: SessionSequence,
510 timestamp: ProtocolTimestamp,
511 causation_id: Option<CausationId>,
512 correlation_id: Option<CorrelationId>,
513 branch_id: Option<BranchId>,
514 metadata: ProtocolMetadata,
515 record: SessionRecord,
516 ) -> Result<Self, RecordValidationError> {
517 let envelope = Self {
518 protocol_version: CURRENT_PROTOCOL_VERSION,
519 record_id,
520 session_id,
521 sequence,
522 timestamp,
523 causation_id,
524 correlation_id,
525 branch_id,
526 metadata,
527 record,
528 };
529 envelope.validate()?;
530 Ok(envelope)
531 }
532
533 pub fn decode_value(value: Value) -> Result<Self, RecordDecodeError> {
540 let version = decode_version(&value).map_err(RecordDecodeError::Invalid)?;
541 if validate_read_version(version).is_err() {
542 return Err(RecordDecodeError::UnsupportedVersion { version });
543 }
544 let discriminator = value
545 .as_object()
546 .and_then(|object| object.get("type"))
547 .and_then(Value::as_str)
548 .ok_or_else(|| RecordDecodeError::Invalid("missing record type".to_owned()))?
549 .to_owned();
550 if SessionRecordTypeText::from_str(&discriminator).is_err() {
551 if valid_discriminator(&discriminator) {
552 return Err(RecordDecodeError::UnsupportedType {
553 record_type: discriminator,
554 });
555 }
556 return Err(RecordDecodeError::Invalid("invalid record type".to_owned()));
557 }
558 serde_json::from_value(value).map_err(|error| RecordDecodeError::Invalid(error.to_string()))
559 }
560
561 #[must_use]
563 pub const fn protocol_version(&self) -> ProtocolVersion {
564 self.protocol_version
565 }
566
567 #[must_use]
569 pub const fn record_id(&self) -> RecordId {
570 self.record_id
571 }
572
573 #[must_use]
575 pub const fn session_id(&self) -> SessionId {
576 self.session_id
577 }
578
579 #[must_use]
581 pub const fn sequence(&self) -> SessionSequence {
582 self.sequence
583 }
584
585 #[must_use]
587 pub const fn timestamp(&self) -> ProtocolTimestamp {
588 self.timestamp
589 }
590
591 #[must_use]
593 pub const fn causation_id(&self) -> Option<CausationId> {
594 self.causation_id
595 }
596
597 #[must_use]
599 pub const fn correlation_id(&self) -> Option<CorrelationId> {
600 self.correlation_id
601 }
602
603 #[must_use]
605 pub const fn branch_id(&self) -> Option<BranchId> {
606 self.branch_id
607 }
608
609 #[must_use]
611 pub const fn metadata(&self) -> &ProtocolMetadata {
612 &self.metadata
613 }
614
615 #[must_use]
617 pub const fn record(&self) -> &SessionRecord {
618 &self.record
619 }
620
621 #[must_use]
623 pub const fn record_type(&self) -> SessionRecordType {
624 self.record.record_type()
625 }
626
627 fn validate(&self) -> Result<(), RecordValidationError> {
628 self.record.validate()?;
629 match &self.record {
630 SessionRecord::BranchCreated { branch_id, .. }
631 | SessionRecord::ActiveBranchChanged { branch_id }
632 if self.branch_id != Some(*branch_id) =>
633 {
634 Err(RecordValidationError::BranchReferenceMismatch)
635 }
636 _ => Ok(()),
637 }
638 }
639}
640
641impl Serialize for RecordEnvelope {
642 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
643 where
644 S: Serializer,
645 {
646 self.validate().map_err(serde::ser::Error::custom)?;
647 let mut value = serde_json::to_value(&self.record).map_err(serde::ser::Error::custom)?;
648 let object = value
649 .as_object_mut()
650 .ok_or_else(|| serde::ser::Error::custom("record must encode as object"))?;
651 object.insert("protocolVersion".to_owned(), json!(self.protocol_version));
652 object.insert("recordId".to_owned(), json!(self.record_id));
653 object.insert("sessionId".to_owned(), json!(self.session_id));
654 object.insert("sequence".to_owned(), json!(self.sequence));
655 object.insert("timestamp".to_owned(), json!(self.timestamp));
656 if let Some(causation_id) = self.causation_id {
657 object.insert("causationId".to_owned(), json!(causation_id));
658 }
659 if let Some(correlation_id) = self.correlation_id {
660 object.insert("correlationId".to_owned(), json!(correlation_id));
661 }
662 if let Some(branch_id) = self.branch_id {
663 object.insert("branchId".to_owned(), json!(branch_id));
664 }
665 if !self.metadata.is_empty() {
666 object.insert("metadata".to_owned(), json!(self.metadata));
667 }
668 value.serialize(serializer)
669 }
670}
671
672impl<'de> Deserialize<'de> for RecordEnvelope {
673 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
674 where
675 D: Deserializer<'de>,
676 {
677 let mut value = deserialize_unique_value(deserializer)?;
678 let object = value
679 .as_object_mut()
680 .ok_or_else(|| serde::de::Error::custom("record envelope must be an object"))?;
681 let protocol_version = take(object, "protocolVersion").map_err(serde::de::Error::custom)?;
682 validate_read_version(protocol_version).map_err(serde::de::Error::custom)?;
683 let envelope = Self {
684 protocol_version,
685 record_id: take(object, "recordId").map_err(serde::de::Error::custom)?,
686 session_id: take(object, "sessionId").map_err(serde::de::Error::custom)?,
687 sequence: take(object, "sequence").map_err(serde::de::Error::custom)?,
688 timestamp: take(object, "timestamp").map_err(serde::de::Error::custom)?,
689 causation_id: take_optional(object, "causationId").map_err(serde::de::Error::custom)?,
690 correlation_id: take_optional(object, "correlationId")
691 .map_err(serde::de::Error::custom)?,
692 branch_id: take_optional(object, "branchId").map_err(serde::de::Error::custom)?,
693 metadata: take_optional(object, "metadata")
694 .map_err(serde::de::Error::custom)?
695 .unwrap_or_default(),
696 record: SessionRecord::deserialize(Value::Object(std::mem::take(object)))
697 .map_err(serde::de::Error::custom)?,
698 };
699 envelope.validate().map_err(serde::de::Error::custom)?;
700 Ok(envelope)
701 }
702}
703
704#[derive(Debug, Error)]
706pub enum RecordDecodeError {
707 #[error("unsupported protocol version: {version}")]
709 UnsupportedVersion {
710 version: ProtocolVersion,
712 },
713 #[error("unsupported required record type: {record_type}")]
715 UnsupportedType {
716 record_type: String,
718 },
719 #[error("invalid durable record: {0}")]
721 Invalid(String),
722}
723
724impl RecordDecodeError {
725 #[must_use]
727 pub fn into_protocol_error(self, correlation_id: CorrelationId) -> ProtocolError {
728 match self {
729 Self::UnsupportedVersion { version } => {
730 ProtocolError::unsupported_protocol_version(correlation_id, version)
731 }
732 Self::UnsupportedType { record_type } => {
733 ProtocolError::unsupported_record(correlation_id, &record_type)
734 }
735 Self::Invalid(_) => ProtocolError::invalid_record(correlation_id),
736 }
737 }
738}
739
740#[derive(Debug, Error)]
742pub enum RecordValidationError {
743 #[error("configuration_changed must include modelId or profileId")]
745 EmptyConfigurationChange,
746 #[error("tool call is invalid: {0}")]
748 InvalidToolCall(#[from] crate::ContentValidationError),
749 #[error("tool arguments must be a JSON object")]
751 ToolArgumentsMustBeObject,
752 #[error("tool arguments exceed protocol bounds: {0}")]
754 ToolArgumentsOutOfBounds(#[from] crate::ProtocolMetadataError),
755 #[error("tool terminal content is invalid")]
757 InvalidToolResultContent,
758 #[error("record envelope branchId must match the branch payload")]
760 BranchReferenceMismatch,
761 #[error("tool terminal isError must match error presence")]
763 InconsistentToolFailure,
764 #[error("failed tool execution cannot include a presentation")]
766 PresentationOnFailure,
767 #[error("interruption reason is invalid")]
769 InvalidInterruptionReason,
770}
771
772#[derive(Debug, Clone, Copy, PartialEq, Eq)]
773struct SessionRecordTypeText;
774
775impl FromStr for SessionRecordTypeText {
776 type Err = ();
777
778 fn from_str(value: &str) -> Result<Self, Self::Err> {
779 if [
780 "session_created",
781 "message_committed",
782 "configuration_changed",
783 "tool_call_requested",
784 "policy_decision_recorded",
785 "approval_requested",
786 "approval_resolved",
787 "tool_execution_started",
788 "tool_execution_finished",
789 "tool_execution_interrupted",
790 "run_interrupted",
791 "run_cancelled",
792 "branch_created",
793 "active_branch_changed",
794 "session_compacted",
795 "turn_checkpointed",
796 ]
797 .contains(&value)
798 {
799 Ok(Self)
800 } else {
801 Err(())
802 }
803 }
804}
805
806fn decode_version(value: &Value) -> Result<ProtocolVersion, String> {
807 let version = value
808 .as_object()
809 .and_then(|object| object.get("protocolVersion"))
810 .cloned()
811 .ok_or_else(|| "missing protocolVersion".to_owned())?;
812 serde_json::from_value(version).map_err(|error| error.to_string())
813}
814
815fn validate_result_content(content: &[ContentBlock]) -> Result<(), RecordValidationError> {
816 if content.is_empty()
817 || content.len() > MAX_RECORD_CONTENT_BLOCKS
818 || !content.iter().all(ContentBlock::valid_for_tool_result)
819 {
820 return Err(RecordValidationError::InvalidToolResultContent);
821 }
822 for block in content {
823 block
824 .validate()
825 .map_err(RecordValidationError::InvalidToolCall)?;
826 }
827 Ok(())
828}
829
830fn validate_reason(reason: &str) -> Result<(), RecordValidationError> {
831 if reason.is_empty() || reason.len() > 4096 || reason.contains('\0') {
832 Err(RecordValidationError::InvalidInterruptionReason)
833 } else {
834 Ok(())
835 }
836}
837
838fn valid_discriminator(value: &str) -> bool {
839 !value.is_empty()
840 && value.len() <= 128
841 && value
842 .bytes()
843 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
844}
845
846fn take<T>(object: &mut Map<String, Value>, key: &str) -> Result<T, serde_json::Error>
847where
848 T: for<'de> Deserialize<'de>,
849{
850 serde_json::from_value(object.remove(key).unwrap_or(Value::Null))
851}
852
853fn take_optional<T>(
854 object: &mut Map<String, Value>,
855 key: &str,
856) -> Result<Option<T>, serde_json::Error>
857where
858 T: for<'de> Deserialize<'de>,
859{
860 object.remove(key).map_or(Ok(None), serde_json::from_value)
861}