Skip to main content

agent_client_protocol_schema/v2/
content.rs

1//! Content blocks for representing various types of information in the Agent Client Protocol.
2//!
3//! This module defines the core content types used throughout the protocol for communication
4//! between agents and clients. Content blocks provide a flexible, extensible way to represent
5//! text, images, audio, and other resources in prompts, responses, and tool call results.
6//!
7//! The content block structure is designed to be compatible with the Model Context Protocol (MCP),
8//! allowing seamless integration between ACP and MCP-based tools.
9//!
10//! See: [Content](https://agentclientprotocol.com/protocol/v2/content)
11
12use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
13
14use derive_more::{Display, From};
15#[cfg(feature = "schemars")]
16use schemars::Schema;
17use serde::{Deserialize, Serialize};
18use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
19
20use super::Meta;
21use crate::IntoOption;
22
23/// An Internet media type identifying the format of protocol content.
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
26#[serde(transparent)]
27#[from(Arc<str>, String, &str, &mut str, Box<str>, Cow<'_, str>)]
28#[non_exhaustive]
29pub struct MediaType(pub Arc<str>);
30
31impl MediaType {
32    /// Wraps a protocol string as a typed [`MediaType`].
33    #[must_use]
34    pub fn new(media_type: impl Into<Self>) -> Self {
35        media_type.into()
36    }
37}
38
39impl AsRef<str> for MediaType {
40    fn as_ref(&self) -> &str {
41        &self.0
42    }
43}
44
45impl From<&String> for MediaType {
46    fn from(media_type: &String) -> Self {
47        Self(media_type.as_str().into())
48    }
49}
50
51macro_rules! impl_media_type_option_conversion {
52    ($source:ty) => {
53        impl IntoOption<MediaType> for $source {
54            fn into_option(self) -> Option<MediaType> {
55                Some(self.into())
56            }
57        }
58    };
59}
60
61impl_media_type_option_conversion!(Arc<str>);
62impl_media_type_option_conversion!(String);
63impl_media_type_option_conversion!(&str);
64impl_media_type_option_conversion!(&mut str);
65impl_media_type_option_conversion!(&String);
66impl_media_type_option_conversion!(Box<str>);
67impl_media_type_option_conversion!(Cow<'_, str>);
68
69/// Content blocks represent displayable information in the Agent Client Protocol.
70///
71/// They provide a structured way to handle various types of user-facing content—whether
72/// it's text from language models, images for analysis, or embedded resources for context.
73///
74/// Content blocks appear in:
75/// - User prompts sent via `session/prompt`
76/// - Language model output reported through `session/update` notifications as
77///   message updates or streamed chunks
78/// - Progress updates and results from tool calls
79///
80/// This structure is compatible with the Model Context Protocol (MCP), enabling
81/// agents to seamlessly forward content from MCP tool outputs without transformation.
82///
83/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/v2/content)
84#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
85#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
86#[serde(tag = "type", rename_all = "snake_case")]
87#[non_exhaustive]
88pub enum ContentBlock {
89    /// Text content. May be plain text or formatted with Markdown.
90    ///
91    /// All agents MUST support text content blocks in prompts.
92    /// Clients SHOULD render this text as CommonMark and MAY support GitHub Flavored Markdown
93    /// extensions.
94    Text(TextContent),
95    /// Images for visual context or analysis.
96    ///
97    /// Requires the `image` prompt capability when included in prompts.
98    Image(ImageContent),
99    /// Audio data for transcription or analysis.
100    ///
101    /// Requires the `audio` prompt capability when included in prompts.
102    Audio(AudioContent),
103    /// References to resources that the agent can access.
104    ///
105    /// All agents MUST support resource links in prompts.
106    ResourceLink(ResourceLink),
107    /// Complete resource contents embedded directly in the message.
108    ///
109    /// Preferred for including context as it avoids extra round-trips.
110    ///
111    /// Requires the `embeddedContext` prompt capability when included in prompts.
112    Resource(EmbeddedResource),
113    /// Custom or future content block.
114    ///
115    /// Values beginning with `_` are reserved for implementation-specific
116    /// extensions. Unknown values that do not begin with `_` are reserved for
117    /// future ACP variants.
118    ///
119    /// Receivers that do not understand this content block type should preserve
120    /// the raw payload when storing, replaying, proxying, or forwarding content,
121    /// and otherwise ignore it or display it generically. Agents MUST reject a
122    /// `session/prompt` request with a content block type they do not support
123    /// with `-32602`.
124    #[serde(untagged)]
125    Other(OtherContentBlock),
126}
127
128/// Custom or future content block payload.
129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
130#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
131#[cfg_attr(feature = "schemars", schemars(inline))]
132#[cfg_attr(feature = "schemars", schemars(transform = other_content_block_schema))]
133#[serde(rename_all = "camelCase")]
134#[non_exhaustive]
135pub struct OtherContentBlock {
136    /// Custom or future content block type.
137    ///
138    /// Values beginning with `_` are reserved for implementation-specific
139    /// extensions. Unknown values that do not begin with `_` are reserved for
140    /// future ACP variants.
141    #[serde(rename = "type")]
142    pub type_: String,
143    /// Additional fields from the unknown content block payload.
144    #[serde(flatten)]
145    pub fields: BTreeMap<String, serde_json::Value>,
146}
147
148impl OtherContentBlock {
149    /// Builds [`OtherContentBlock`] from an unknown discriminator and preserves the remaining extension fields.
150    #[must_use]
151    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
152        fields.remove("type");
153        Self {
154            type_: type_.into(),
155            fields,
156        }
157    }
158}
159
160impl<'de> Deserialize<'de> for OtherContentBlock {
161    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162    where
163        D: serde::Deserializer<'de>,
164    {
165        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
166        let type_ = fields
167            .remove("type")
168            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
169        let serde_json::Value::String(type_) = type_ else {
170            return Err(serde::de::Error::custom("`type` must be a string"));
171        };
172
173        if is_known_content_block_type(&type_) {
174            return Err(serde::de::Error::custom(format!(
175                "known content block `{type_}` did not match its schema"
176            )));
177        }
178
179        Ok(Self { type_, fields })
180    }
181}
182
183fn is_known_content_block_type(type_: &str) -> bool {
184    matches!(
185        type_,
186        "text" | "image" | "audio" | "resource_link" | "resource"
187    )
188}
189
190#[cfg(feature = "schemars")]
191fn other_content_block_schema(schema: &mut Schema) {
192    super::schema_util::reject_known_string_discriminators(
193        schema,
194        "type",
195        &["text", "image", "audio", "resource_link", "resource"],
196    );
197}
198
199/// Text provided to or from an LLM.
200#[serde_as]
201#[skip_serializing_none]
202#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
203#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
204#[non_exhaustive]
205pub struct TextContent {
206    /// Text payload carried by this content block.
207    pub text: String,
208    /// Optional annotations that help clients decide how to display or route this content.
209    ///
210    /// Omitted and `null` are equivalent and mean no annotations are provided.
211    #[serde_as(deserialize_as = "DefaultOnError")]
212    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
213    #[serde(default)]
214    pub annotations: Option<Annotations>,
215    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
216    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
217    /// Implementations MUST NOT make assumptions about values at these keys.
218    ///
219    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
220    #[serde_as(deserialize_as = "DefaultOnError")]
221    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
222    #[serde(default)]
223    #[serde(rename = "_meta")]
224    pub meta: Option<Meta>,
225}
226
227impl TextContent {
228    /// Builds [`TextContent`] with its required content payload; optional annotations and metadata start unset.
229    #[must_use]
230    pub fn new(text: impl Into<String>) -> Self {
231        Self {
232            annotations: None,
233            text: text.into(),
234            meta: None,
235        }
236    }
237
238    /// Sets or clears the optional `annotations` field.
239    #[must_use]
240    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
241        self.annotations = annotations.into_option();
242        self
243    }
244
245    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
246    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
247    /// these keys.
248    ///
249    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
250    #[must_use]
251    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
252        self.meta = meta.into_option();
253        self
254    }
255}
256
257impl<T: Into<String>> From<T> for ContentBlock {
258    fn from(value: T) -> Self {
259        Self::Text(TextContent::new(value))
260    }
261}
262
263/// An image provided to or from an LLM.
264#[serde_as]
265#[skip_serializing_none]
266#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
267#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
268#[serde(rename_all = "camelCase")]
269#[non_exhaustive]
270pub struct ImageContent {
271    /// Base64-encoded media payload.
272    #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
273    pub data: String,
274    /// MIME type describing the encoded media payload.
275    pub mime_type: MediaType,
276    /// URI associated with this resource or media payload.
277    ///
278    /// Omitted and `null` are equivalent and mean no URI is associated with the image.
279    #[serde_as(deserialize_as = "DefaultOnError")]
280    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
281    #[cfg_attr(feature = "schemars", schemars(url))]
282    #[serde(default)]
283    pub uri: Option<String>,
284    /// Optional annotations that help clients decide how to display or route this content.
285    ///
286    /// Omitted and `null` are equivalent and mean no annotations are provided.
287    #[serde_as(deserialize_as = "DefaultOnError")]
288    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
289    #[serde(default)]
290    pub annotations: Option<Annotations>,
291    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
292    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
293    /// Implementations MUST NOT make assumptions about values at these keys.
294    ///
295    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
296    #[serde_as(deserialize_as = "DefaultOnError")]
297    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
298    #[serde(default)]
299    #[serde(rename = "_meta")]
300    pub meta: Option<Meta>,
301}
302
303impl ImageContent {
304    /// Builds [`ImageContent`] with its required content payload; optional annotations and metadata start unset.
305    #[must_use]
306    pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
307        Self {
308            annotations: None,
309            data: data.into(),
310            mime_type: mime_type.into(),
311            uri: None,
312            meta: None,
313        }
314    }
315
316    /// Sets or clears the optional `annotations` field.
317    #[must_use]
318    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
319        self.annotations = annotations.into_option();
320        self
321    }
322
323    /// Sets or clears the optional `uri` field.
324    #[must_use]
325    pub fn uri(mut self, uri: impl IntoOption<String>) -> Self {
326        self.uri = uri.into_option();
327        self
328    }
329
330    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
331    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
332    /// these keys.
333    ///
334    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
335    #[must_use]
336    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
337        self.meta = meta.into_option();
338        self
339    }
340}
341
342/// Audio provided to or from an LLM.
343#[serde_as]
344#[skip_serializing_none]
345#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
346#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
347#[serde(rename_all = "camelCase")]
348#[non_exhaustive]
349pub struct AudioContent {
350    /// Base64-encoded media payload.
351    #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
352    pub data: String,
353    /// MIME type describing the encoded media payload.
354    pub mime_type: MediaType,
355    /// Optional annotations that help clients decide how to display or route this content.
356    ///
357    /// Omitted and `null` are equivalent and mean no annotations are provided.
358    #[serde_as(deserialize_as = "DefaultOnError")]
359    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
360    #[serde(default)]
361    pub annotations: Option<Annotations>,
362    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
363    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
364    /// Implementations MUST NOT make assumptions about values at these keys.
365    ///
366    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
367    #[serde_as(deserialize_as = "DefaultOnError")]
368    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
369    #[serde(default)]
370    #[serde(rename = "_meta")]
371    pub meta: Option<Meta>,
372}
373
374impl AudioContent {
375    /// Builds [`AudioContent`] with its required content payload; optional annotations and metadata start unset.
376    #[must_use]
377    pub fn new(data: impl Into<String>, mime_type: impl Into<MediaType>) -> Self {
378        Self {
379            annotations: None,
380            data: data.into(),
381            mime_type: mime_type.into(),
382            meta: None,
383        }
384    }
385
386    /// Sets or clears the optional `annotations` field.
387    #[must_use]
388    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
389        self.annotations = annotations.into_option();
390        self
391    }
392
393    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
394    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
395    /// these keys.
396    ///
397    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
398    #[must_use]
399    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
400        self.meta = meta.into_option();
401        self
402    }
403}
404
405/// The contents of a resource, embedded into a prompt or tool call result.
406#[serde_as]
407#[skip_serializing_none]
408#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
409#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
410#[non_exhaustive]
411pub struct EmbeddedResource {
412    /// Embedded resource payload, either text or binary data.
413    pub resource: EmbeddedResourceResource,
414    /// Optional annotations that help clients decide how to display or route this content.
415    ///
416    /// Omitted and `null` are equivalent and mean no annotations are provided.
417    #[serde_as(deserialize_as = "DefaultOnError")]
418    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
419    #[serde(default)]
420    pub annotations: Option<Annotations>,
421    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
422    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
423    /// Implementations MUST NOT make assumptions about values at these keys.
424    ///
425    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
426    #[serde_as(deserialize_as = "DefaultOnError")]
427    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
428    #[serde(default)]
429    #[serde(rename = "_meta")]
430    pub meta: Option<Meta>,
431}
432
433impl EmbeddedResource {
434    /// Builds [`EmbeddedResource`] with its required content payload; optional annotations and metadata start unset.
435    #[must_use]
436    pub fn new(resource: EmbeddedResourceResource) -> Self {
437        Self {
438            annotations: None,
439            resource,
440            meta: None,
441        }
442    }
443
444    /// Sets or clears the optional `annotations` field.
445    #[must_use]
446    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
447        self.annotations = annotations.into_option();
448        self
449    }
450
451    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
452    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
453    /// these keys.
454    ///
455    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
456    #[must_use]
457    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
458        self.meta = meta.into_option();
459        self
460    }
461}
462
463/// Resource content that can be embedded in a message.
464#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
465#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
466#[serde(untagged)]
467#[non_exhaustive]
468pub enum EmbeddedResourceResource {
469    /// Text resource contents embedded directly in the message.
470    TextResourceContents(TextResourceContents),
471    /// Binary resource contents embedded directly in the message.
472    BlobResourceContents(BlobResourceContents),
473}
474
475/// Text-based resource contents.
476#[serde_as]
477#[skip_serializing_none]
478#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
479#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
480#[serde(rename_all = "camelCase")]
481#[non_exhaustive]
482pub struct TextResourceContents {
483    /// Text payload carried by this content block.
484    pub text: String,
485    /// URI associated with this resource or media payload.
486    #[cfg_attr(feature = "schemars", schemars(url))]
487    pub uri: String,
488    /// MIME type describing the encoded media payload.
489    ///
490    /// Omitted and `null` are equivalent and mean the MIME type is not specified.
491    #[serde_as(deserialize_as = "DefaultOnError")]
492    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
493    #[serde(default)]
494    pub mime_type: Option<MediaType>,
495    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
496    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
497    /// Implementations MUST NOT make assumptions about values at these keys.
498    ///
499    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
500    #[serde_as(deserialize_as = "DefaultOnError")]
501    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
502    #[serde(default)]
503    #[serde(rename = "_meta")]
504    pub meta: Option<Meta>,
505}
506
507impl TextResourceContents {
508    /// Builds [`TextResourceContents`] with its required content payload; optional annotations and metadata start unset.
509    #[must_use]
510    pub fn new(text: impl Into<String>, uri: impl Into<String>) -> Self {
511        Self {
512            mime_type: None,
513            text: text.into(),
514            uri: uri.into(),
515            meta: None,
516        }
517    }
518
519    /// Sets or clears the optional `mimeType` field.
520    #[must_use]
521    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
522        self.mime_type = mime_type.into_option();
523        self
524    }
525
526    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
527    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
528    /// these keys.
529    ///
530    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
531    #[must_use]
532    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
533        self.meta = meta.into_option();
534        self
535    }
536}
537
538/// Binary resource contents.
539#[serde_as]
540#[skip_serializing_none]
541#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
543#[serde(rename_all = "camelCase")]
544#[non_exhaustive]
545pub struct BlobResourceContents {
546    /// Base64-encoded bytes for a binary resource payload.
547    #[cfg_attr(feature = "schemars", schemars(extend("contentEncoding" = "base64")))]
548    pub blob: String,
549    /// URI associated with this resource or media payload.
550    #[cfg_attr(feature = "schemars", schemars(url))]
551    pub uri: String,
552    /// MIME type describing the encoded media payload.
553    ///
554    /// Omitted and `null` are equivalent and mean the MIME type is not specified.
555    #[serde_as(deserialize_as = "DefaultOnError")]
556    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
557    #[serde(default)]
558    pub mime_type: Option<MediaType>,
559    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
560    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
561    /// Implementations MUST NOT make assumptions about values at these keys.
562    ///
563    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
564    #[serde_as(deserialize_as = "DefaultOnError")]
565    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
566    #[serde(default)]
567    #[serde(rename = "_meta")]
568    pub meta: Option<Meta>,
569}
570
571impl BlobResourceContents {
572    /// Builds [`BlobResourceContents`] with its required content payload; optional annotations and metadata start unset.
573    #[must_use]
574    pub fn new(blob: impl Into<String>, uri: impl Into<String>) -> Self {
575        Self {
576            blob: blob.into(),
577            mime_type: None,
578            uri: uri.into(),
579            meta: None,
580        }
581    }
582
583    /// Sets or clears the optional `mimeType` field.
584    #[must_use]
585    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
586        self.mime_type = mime_type.into_option();
587        self
588    }
589
590    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
591    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
592    /// these keys.
593    ///
594    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
595    #[must_use]
596    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
597        self.meta = meta.into_option();
598        self
599    }
600}
601
602/// A resource that the server is capable of reading, included in a prompt or tool call result.
603#[serde_as]
604#[skip_serializing_none]
605#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
606#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
607#[serde(rename_all = "camelCase")]
608#[non_exhaustive]
609pub struct ResourceLink {
610    /// Human-readable name shown for this protocol object.
611    pub name: String,
612    /// URI associated with this resource or media payload.
613    #[cfg_attr(feature = "schemars", schemars(url))]
614    pub uri: String,
615    /// Optional display title for end-user UI.
616    ///
617    /// Omitted and `null` are equivalent and mean no title is provided.
618    #[serde_as(deserialize_as = "DefaultOnError")]
619    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
620    #[serde(default)]
621    pub title: Option<String>,
622    /// Optional human-readable details shown with this protocol object.
623    ///
624    /// Omitted and `null` are equivalent and mean no description is provided.
625    #[serde_as(deserialize_as = "DefaultOnError")]
626    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
627    #[serde(default)]
628    pub description: Option<String>,
629    /// Optional set of sized icons that the client can display in a user interface.
630    ///
631    /// Omitted and `null` are equivalent and mean no icons are provided.
632    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
633    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
634    #[serde(default)]
635    pub icons: Option<Vec<Icon>>,
636    /// MIME type describing the encoded media payload.
637    ///
638    /// Omitted and `null` are equivalent and mean the MIME type is not specified.
639    #[serde_as(deserialize_as = "DefaultOnError")]
640    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
641    #[serde(default)]
642    pub mime_type: Option<MediaType>,
643    /// Optional size of the linked resource in bytes, if known.
644    ///
645    /// Omitted and `null` are equivalent and mean the size is not known.
646    #[serde_as(deserialize_as = "DefaultOnError")]
647    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
648    #[serde(default)]
649    pub size: Option<u64>,
650    /// Optional annotations that help clients decide how to display or route this content.
651    ///
652    /// Omitted and `null` are equivalent and mean no annotations are provided.
653    #[serde_as(deserialize_as = "DefaultOnError")]
654    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
655    #[serde(default)]
656    pub annotations: Option<Annotations>,
657    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
658    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
659    /// Implementations MUST NOT make assumptions about values at these keys.
660    ///
661    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
662    #[serde_as(deserialize_as = "DefaultOnError")]
663    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
664    #[serde(default)]
665    #[serde(rename = "_meta")]
666    pub meta: Option<Meta>,
667}
668
669impl ResourceLink {
670    /// Builds [`ResourceLink`] with its required content payload; optional annotations and metadata start unset.
671    #[must_use]
672    pub fn new(name: impl Into<String>, uri: impl Into<String>) -> Self {
673        Self {
674            annotations: None,
675            description: None,
676            icons: None,
677            mime_type: None,
678            name: name.into(),
679            size: None,
680            title: None,
681            uri: uri.into(),
682            meta: None,
683        }
684    }
685
686    /// Sets or clears the optional `annotations` field.
687    #[must_use]
688    pub fn annotations(mut self, annotations: impl IntoOption<Annotations>) -> Self {
689        self.annotations = annotations.into_option();
690        self
691    }
692
693    /// Sets or clears the optional `description` field.
694    #[must_use]
695    pub fn description(mut self, description: impl IntoOption<String>) -> Self {
696        self.description = description.into_option();
697        self
698    }
699
700    /// Sets or clears the optional `icons` field.
701    #[must_use]
702    pub fn icons(mut self, icons: impl IntoOption<Vec<Icon>>) -> Self {
703        self.icons = icons.into_option();
704        self
705    }
706
707    /// Sets or clears the optional `mimeType` field.
708    #[must_use]
709    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
710        self.mime_type = mime_type.into_option();
711        self
712    }
713
714    /// Sets or clears the optional `size` field.
715    #[must_use]
716    pub fn size(mut self, size: impl IntoOption<u64>) -> Self {
717        self.size = size.into_option();
718        self
719    }
720
721    /// Sets or clears the optional `title` field.
722    #[must_use]
723    pub fn title(mut self, title: impl IntoOption<String>) -> Self {
724        self.title = title.into_option();
725        self
726    }
727
728    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
729    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
730    /// these keys.
731    ///
732    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
733    #[must_use]
734    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
735        self.meta = meta.into_option();
736        self
737    }
738}
739
740/// An optionally-sized icon that can be displayed in a user interface.
741#[serde_as]
742#[skip_serializing_none]
743#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
744#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
745#[serde(rename_all = "camelCase")]
746#[non_exhaustive]
747pub struct Icon {
748    /// A standard URI pointing to an icon resource.
749    #[cfg_attr(feature = "schemars", schemars(url))]
750    pub src: String,
751    /// Optional MIME type override if the source MIME type is missing or generic.
752    ///
753    /// Omitted and `null` are equivalent and mean the source MIME type is used.
754    #[serde_as(deserialize_as = "DefaultOnError")]
755    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
756    #[serde(default)]
757    pub mime_type: Option<MediaType>,
758    /// Optional array of strings that specify sizes at which the icon can be used.
759    /// Each string should be in `WxH` format (e.g., `"48x48"`, `"96x96"`) or
760    /// `"any"` for scalable formats like SVG.
761    ///
762    /// If not provided, the client should assume that the icon can be used at any size.
763    ///
764    /// Omitted and `null` are equivalent and mean no sizes are specified.
765    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
766    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
767    #[serde(default)]
768    pub sizes: Option<Vec<String>>,
769    /// Optional theme this icon is designed for.
770    ///
771    /// Omitted and `null` are equivalent and mean the icon is not theme-specific.
772    #[serde_as(deserialize_as = "DefaultOnError")]
773    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
774    #[serde(default)]
775    pub theme: Option<IconTheme>,
776    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
777    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
778    /// Implementations MUST NOT make assumptions about values at these keys.
779    ///
780    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
781    #[serde_as(deserialize_as = "DefaultOnError")]
782    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
783    #[serde(default)]
784    #[serde(rename = "_meta")]
785    pub meta: Option<Meta>,
786}
787
788impl Icon {
789    /// Builds [`Icon`] with the required source URI; optional display hints start unset.
790    #[must_use]
791    pub fn new(src: impl Into<String>) -> Self {
792        Self {
793            src: src.into(),
794            mime_type: None,
795            sizes: None,
796            theme: None,
797            meta: None,
798        }
799    }
800
801    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
802    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
803    /// these keys.
804    ///
805    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
806    #[must_use]
807    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
808        self.meta = meta.into_option();
809        self
810    }
811
812    /// Sets or clears the optional `mimeType` field.
813    #[must_use]
814    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
815        self.mime_type = mime_type.into_option();
816        self
817    }
818
819    /// Sets or clears the optional sizes at which the icon can be used.
820    #[must_use]
821    pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
822        self.sizes = sizes.into_option();
823        self
824    }
825
826    /// Sets or clears the optional `theme` field.
827    #[must_use]
828    pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
829        self.theme = theme.into_option();
830        self
831    }
832}
833
834/// Theme an icon is designed for.
835#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
836#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
837#[serde(rename_all = "camelCase")]
838#[non_exhaustive]
839pub enum IconTheme {
840    /// Icon designed for light backgrounds.
841    Light,
842    /// Icon designed for dark backgrounds.
843    Dark,
844    /// Custom or future icon theme.
845    ///
846    /// Values beginning with `_` are reserved for implementation-specific
847    /// extensions. Unknown values that do not begin with `_` are reserved for
848    /// future ACP variants.
849    #[serde(untagged)]
850    Other(String),
851}
852
853/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
854#[serde_as]
855#[skip_serializing_none]
856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
857#[derive(Debug, Clone, Deserialize, Serialize, Default)]
858#[serde(rename_all = "camelCase")]
859#[non_exhaustive]
860pub struct Annotations {
861    /// Intended recipients for this content, such as the user or assistant.
862    ///
863    /// Omitted and `null` are equivalent and mean no audience is specified.
864    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
865    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
866    #[serde(default)]
867    pub audience: Option<Vec<Role>>,
868    /// Timestamp indicating when the underlying resource was last modified.
869    ///
870    /// Must be an RFC 3339 formatted string (e.g., "2025-01-12T15:00:58Z").
871    ///
872    /// Omitted and `null` are equivalent and mean the modification time is not reported.
873    #[serde_as(deserialize_as = "DefaultOnError")]
874    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "format" = "date-time")))]
875    #[serde(default)]
876    pub last_modified: Option<String>,
877    /// Relative importance of this content when clients choose what to surface.
878    ///
879    /// Omitted and `null` are equivalent and mean no priority is specified.
880    #[serde_as(deserialize_as = "DefaultOnError")]
881    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
882    #[cfg_attr(feature = "schemars", schemars(range(min = 0, max = 1)))]
883    #[serde(default)]
884    pub priority: Option<f64>,
885    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
886    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
887    /// Implementations MUST NOT make assumptions about values at these keys.
888    ///
889    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
890    #[serde_as(deserialize_as = "DefaultOnError")]
891    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
892    #[serde(default)]
893    #[serde(rename = "_meta")]
894    pub meta: Option<Meta>,
895}
896
897/// Priorities are compared with [`f64::total_cmp`] (the IEEE 754 total order), so
898/// equality is reflexive even for NaN and annotations, and the content that
899/// carries them, can be `Eq`. Unlike `==`, it treats `0.0` and `-0.0` as
900/// different, matching how they serialize.
901impl PartialEq for Annotations {
902    fn eq(&self, other: &Self) -> bool {
903        let Self {
904            audience,
905            last_modified,
906            priority,
907            meta,
908        } = self;
909        *audience == other.audience
910            && *last_modified == other.last_modified
911            && priority_eq(*priority, other.priority)
912            && *meta == other.meta
913    }
914}
915
916impl Eq for Annotations {}
917
918fn priority_eq(a: Option<f64>, b: Option<f64>) -> bool {
919    match (a, b) {
920        (Some(a), Some(b)) => a.total_cmp(&b).is_eq(),
921        (None, None) => true,
922        _ => false,
923    }
924}
925
926impl Annotations {
927    /// Creates annotations with no audience, priority, or timestamp hints set.
928    #[must_use]
929    pub fn new() -> Self {
930        Self::default()
931    }
932
933    /// Sets or clears the optional `audience` field.
934    #[must_use]
935    pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
936        self.audience = audience.into_option();
937        self
938    }
939
940    /// Sets or clears the optional `lastModified` field.
941    #[must_use]
942    pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
943        self.last_modified = last_modified.into_option();
944        self
945    }
946
947    /// Sets or clears the optional `priority` field.
948    #[must_use]
949    pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
950        self.priority = priority.into_option();
951        self
952    }
953
954    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
955    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
956    /// these keys.
957    ///
958    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
959    #[must_use]
960    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
961        self.meta = meta.into_option();
962        self
963    }
964}
965
966/// The sender or recipient of messages and data in a conversation.
967#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
968#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
969#[serde(rename_all = "camelCase")]
970#[non_exhaustive]
971pub enum Role {
972    /// The assistant side of a conversation.
973    Assistant,
974    /// The user side of a conversation.
975    User,
976    /// Custom or future role.
977    ///
978    /// Values beginning with `_` are reserved for implementation-specific
979    /// extensions. Unknown values that do not begin with `_` are reserved for
980    /// future ACP variants.
981    #[serde(untagged)]
982    Other(String),
983}
984
985#[cfg(test)]
986mod tests {
987    use super::*;
988
989    #[test]
990    fn test_text_content_roundtrip() {
991        let content = TextContent::new("hello world");
992        let json = serde_json::to_value(&content).unwrap();
993        let parsed: TextContent = serde_json::from_value(json).unwrap();
994        assert_eq!(content, parsed);
995    }
996
997    #[test]
998    fn test_annotations_equality_is_reflexive() {
999        let nan = Annotations::new().priority(f64::NAN);
1000        assert_eq!(nan, nan.clone());
1001        assert_eq!(
1002            Annotations::new().priority(0.5),
1003            Annotations::new().priority(0.5)
1004        );
1005        assert_ne!(
1006            Annotations::new().priority(0.0),
1007            Annotations::new().priority(-0.0)
1008        );
1009        assert_ne!(
1010            serde_json::to_string(&Annotations::new().priority(0.0)).unwrap(),
1011            serde_json::to_string(&Annotations::new().priority(-0.0)).unwrap()
1012        );
1013        assert_ne!(
1014            Annotations::new().priority(0.5),
1015            Annotations::new().priority(1.0)
1016        );
1017        assert_ne!(nan, Annotations::new().priority(0.5));
1018        assert_ne!(Annotations::new().priority(0.5), Annotations::new());
1019        assert_ne!(
1020            Annotations::new().priority(0.5).audience(vec![Role::User]),
1021            Annotations::new().priority(0.5)
1022        );
1023    }
1024
1025    #[test]
1026    fn test_text_content_omits_optional_fields() {
1027        let content = TextContent::new("hello");
1028        let json = serde_json::to_value(&content).unwrap();
1029        assert!(!json.as_object().unwrap().contains_key("annotations"));
1030        assert!(!json.as_object().unwrap().contains_key("meta"));
1031    }
1032
1033    #[test]
1034    fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
1035        let missing: TextContent = serde_json::from_value(serde_json::json!({
1036            "text": "hello"
1037        }))
1038        .unwrap();
1039        assert_eq!(missing.meta, None);
1040
1041        let malformed: TextContent = serde_json::from_value(serde_json::json!({
1042            "text": "hello",
1043            "_meta": false
1044        }))
1045        .unwrap();
1046        assert_eq!(malformed.meta, None);
1047    }
1048
1049    #[test]
1050    fn test_text_content_from_string() {
1051        let block: ContentBlock = "hello".into();
1052        match block {
1053            ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
1054            _ => panic!("Expected Text variant"),
1055        }
1056    }
1057
1058    #[test]
1059    fn role_preserves_unknown_variant() {
1060        let role: Role = serde_json::from_str("\"critic\"").unwrap();
1061        assert_eq!(role, Role::Other("critic".to_string()));
1062        assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
1063    }
1064
1065    #[test]
1066    fn icon_theme_preserves_unknown_variant() {
1067        let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
1068        assert_eq!(theme, IconTheme::Other("contrast".to_string()));
1069        assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
1070    }
1071
1072    #[test]
1073    fn content_block_preserves_unknown_variant() {
1074        let block: ContentBlock = serde_json::from_value(serde_json::json!({
1075            "type": "_widget",
1076            "title": "Status",
1077            "state": {"ok": true}
1078        }))
1079        .unwrap();
1080
1081        let ContentBlock::Other(unknown) = block else {
1082            panic!("expected unknown content block");
1083        };
1084
1085        assert_eq!(unknown.type_, "_widget");
1086        assert_eq!(
1087            unknown.fields.get("title"),
1088            Some(&serde_json::json!("Status"))
1089        );
1090        assert_eq!(
1091            serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
1092            serde_json::json!({
1093                "type": "_widget",
1094                "title": "Status",
1095                "state": {"ok": true}
1096            })
1097        );
1098    }
1099
1100    #[test]
1101    fn content_block_does_not_hide_malformed_known_variant() {
1102        assert!(
1103            serde_json::from_value::<ContentBlock>(serde_json::json!({
1104                "type": "text"
1105            }))
1106            .is_err()
1107        );
1108    }
1109
1110    #[test]
1111    fn test_image_content_roundtrip() {
1112        let content = ImageContent::new("base64data", "image/png");
1113        let json = serde_json::to_value(&content).unwrap();
1114        let parsed: ImageContent = serde_json::from_value(json).unwrap();
1115        assert_eq!(content, parsed);
1116    }
1117
1118    #[test]
1119    fn test_image_content_omits_optional_fields() {
1120        let content = ImageContent::new("data", "image/png");
1121        let json = serde_json::to_value(&content).unwrap();
1122        assert!(!json.as_object().unwrap().contains_key("uri"));
1123        assert!(!json.as_object().unwrap().contains_key("annotations"));
1124        assert!(!json.as_object().unwrap().contains_key("meta"));
1125    }
1126
1127    #[test]
1128    fn test_image_content_with_uri() {
1129        let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
1130        let json = serde_json::to_value(&content).unwrap();
1131        assert_eq!(json["uri"], "https://example.com/image.png");
1132    }
1133
1134    #[test]
1135    fn test_audio_content_roundtrip() {
1136        let content = AudioContent::new("base64audio", "audio/mp3");
1137        let json = serde_json::to_value(&content).unwrap();
1138        let parsed: AudioContent = serde_json::from_value(json).unwrap();
1139        assert_eq!(content, parsed);
1140    }
1141
1142    #[test]
1143    fn test_audio_content_omits_optional_fields() {
1144        let content = AudioContent::new("data", "audio/mp3");
1145        let json = serde_json::to_value(&content).unwrap();
1146        assert!(!json.as_object().unwrap().contains_key("annotations"));
1147        assert!(!json.as_object().unwrap().contains_key("meta"));
1148    }
1149
1150    #[test]
1151    fn resource_link_icons_roundtrip() {
1152        let icon = Icon::new("https://example.com/icon.png")
1153            .mime_type("image/png")
1154            .sizes(vec!["48x48".to_string(), "any".to_string()])
1155            .theme(IconTheme::Dark);
1156        let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1157
1158        let json = serde_json::to_value(&link).unwrap();
1159        assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1160        assert_eq!(json["icons"][0]["mimeType"], "image/png");
1161        assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1162        assert_eq!(json["icons"][0]["theme"], "dark");
1163
1164        let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1165        assert_eq!(link, parsed);
1166    }
1167
1168    #[cfg(feature = "schemars")]
1169    #[test]
1170    fn annotations_priority_schema_matches_mcp_bounds() {
1171        let schema = schemars::schema_for!(Annotations);
1172        let json = serde_json::to_value(schema).unwrap();
1173
1174        assert_eq!(json["properties"]["priority"]["minimum"], 0);
1175        assert_eq!(json["properties"]["priority"]["maximum"], 1);
1176        assert_eq!(json["properties"]["lastModified"]["format"], "date-time");
1177    }
1178
1179    #[cfg(feature = "schemars")]
1180    #[test]
1181    fn content_schema_uses_standard_string_annotations() {
1182        let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1183        assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1184        assert!(image["properties"]["data"].get("format").is_none());
1185        assert_eq!(image["properties"]["uri"]["format"], "uri");
1186
1187        let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1188        assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1189        assert!(audio["properties"]["data"].get("format").is_none());
1190
1191        let text_resource =
1192            serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1193        assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1194
1195        let blob_resource =
1196            serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1197        assert_eq!(
1198            blob_resource["properties"]["blob"]["contentEncoding"],
1199            "base64"
1200        );
1201        assert!(blob_resource["properties"]["blob"].get("format").is_none());
1202        assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1203
1204        let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1205        assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1206
1207        let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1208        assert_eq!(icon["properties"]["src"]["format"], "uri");
1209        assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1210        assert!(
1211            icon["properties"]["sizes"]["items"]
1212                .get("pattern")
1213                .is_none()
1214        );
1215    }
1216}