Skip to main content

agent_client_protocol_schema/v2/
content.rs

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