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 receiver may be able to read.
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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
247    /// do not recognize.
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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
332    /// do not recognize.
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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
395    /// do not recognize.
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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
453    /// do not recognize.
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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
528    /// do not recognize.
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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
592    /// do not recognize.
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 link to a resource that the receiver may be able to read, 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 assign meaning to keys they do not recognize.
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 assign meaning to keys they
730    /// do not recognize.
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. MUST be an `https:` or `data:` URI;
749    /// Clients reject other schemes, fetch without credentials, and do not follow
750    /// scheme- or origin-changing redirects.
751    #[cfg_attr(feature = "schemars", schemars(url))]
752    pub src: String,
753    /// Optional MIME type override if the source MIME type is missing or generic.
754    ///
755    /// Omitted and `null` are equivalent and mean the source MIME type is used.
756    #[serde_as(deserialize_as = "DefaultOnError")]
757    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
758    #[serde(default)]
759    pub mime_type: Option<MediaType>,
760    /// Optional array of strings that specify sizes at which the icon can be used.
761    /// Each string should be in `WxH` format (e.g., `"48x48"`, `"96x96"`) or
762    /// `"any"` for scalable formats like SVG.
763    ///
764    /// If not provided, the client should assume that the icon can be used at any size.
765    ///
766    /// Omitted and `null` are equivalent and mean no sizes are specified.
767    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
768    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
769    #[serde(default)]
770    pub sizes: Option<Vec<String>>,
771    /// Optional theme this icon is designed for.
772    ///
773    /// Omitted and `null` are equivalent and mean the icon is not theme-specific.
774    #[serde_as(deserialize_as = "DefaultOnError")]
775    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
776    #[serde(default)]
777    pub theme: Option<IconTheme>,
778    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
779    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
780    /// Implementations MUST NOT assign meaning to keys they do not recognize.
781    ///
782    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
783    #[serde_as(deserialize_as = "DefaultOnError")]
784    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
785    #[serde(default)]
786    #[serde(rename = "_meta")]
787    pub meta: Option<Meta>,
788}
789
790impl Icon {
791    /// Builds [`Icon`] with the required source URI; optional display hints start unset.
792    #[must_use]
793    pub fn new(src: impl Into<String>) -> Self {
794        Self {
795            src: src.into(),
796            mime_type: None,
797            sizes: None,
798            theme: None,
799            meta: None,
800        }
801    }
802
803    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
804    /// metadata to their interactions. Implementations MUST NOT assign meaning to keys they
805    /// do not recognize.
806    ///
807    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
808    #[must_use]
809    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
810        self.meta = meta.into_option();
811        self
812    }
813
814    /// Sets or clears the optional `mimeType` field.
815    #[must_use]
816    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
817        self.mime_type = mime_type.into_option();
818        self
819    }
820
821    /// Sets or clears the optional sizes at which the icon can be used.
822    #[must_use]
823    pub fn sizes(mut self, sizes: impl IntoOption<Vec<String>>) -> Self {
824        self.sizes = sizes.into_option();
825        self
826    }
827
828    /// Sets or clears the optional `theme` field.
829    #[must_use]
830    pub fn theme(mut self, theme: impl IntoOption<IconTheme>) -> Self {
831        self.theme = theme.into_option();
832        self
833    }
834}
835
836/// Theme an icon is designed for.
837#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
838#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
839#[serde(rename_all = "camelCase")]
840#[non_exhaustive]
841pub enum IconTheme {
842    /// Icon designed for light backgrounds.
843    Light,
844    /// Icon designed for dark backgrounds.
845    Dark,
846    /// Custom or future icon theme.
847    ///
848    /// Values beginning with `_` are reserved for implementation-specific
849    /// extensions. Unknown values that do not begin with `_` are reserved for
850    /// future ACP variants.
851    #[serde(untagged)]
852    Other(String),
853}
854
855/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
856#[serde_as]
857#[skip_serializing_none]
858#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
859#[derive(Debug, Clone, Deserialize, Serialize, Default)]
860#[serde(rename_all = "camelCase")]
861#[non_exhaustive]
862pub struct Annotations {
863    /// Intended recipients for this content, such as the user or assistant.
864    ///
865    /// Omitted and `null` are equivalent and mean no audience is specified.
866    #[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_>>>")]
867    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
868    #[serde(default)]
869    pub audience: Option<Vec<Role>>,
870    /// Timestamp indicating when the underlying resource was last modified.
871    ///
872    /// Should be an ISO 8601 formatted string (e.g., "2025-01-12T15:00:58Z"), as in MCP.
873    ///
874    /// Omitted and `null` are equivalent and mean the modification time is not reported.
875    #[serde_as(deserialize_as = "DefaultOnError")]
876    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
877    #[serde(default)]
878    pub last_modified: Option<String>,
879    /// Relative importance of this content when clients choose what to surface, from 0 (least
880    /// important) to 1 (most important).
881    ///
882    /// Omitted and `null` are equivalent and mean no priority is specified.
883    #[serde_as(deserialize_as = "DefaultOnError")]
884    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
885    #[cfg_attr(feature = "schemars", schemars(range(min = 0, max = 1)))]
886    #[serde(default)]
887    pub priority: Option<f64>,
888    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
889    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
890    /// Implementations MUST NOT assign meaning to keys they do not recognize.
891    ///
892    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
893    #[serde_as(deserialize_as = "DefaultOnError")]
894    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
895    #[serde(default)]
896    #[serde(rename = "_meta")]
897    pub meta: Option<Meta>,
898}
899
900/// Priorities are compared with [`f64::total_cmp`] (the IEEE 754 total order), so
901/// equality is reflexive even for NaN and annotations, and the content that
902/// carries them, can be `Eq`. Unlike `==`, it treats `0.0` and `-0.0` as
903/// different, matching how they serialize.
904impl PartialEq for Annotations {
905    fn eq(&self, other: &Self) -> bool {
906        let Self {
907            audience,
908            last_modified,
909            priority,
910            meta,
911        } = self;
912        *audience == other.audience
913            && *last_modified == other.last_modified
914            && priority_eq(*priority, other.priority)
915            && *meta == other.meta
916    }
917}
918
919impl Eq for Annotations {}
920
921fn priority_eq(a: Option<f64>, b: Option<f64>) -> bool {
922    match (a, b) {
923        (Some(a), Some(b)) => a.total_cmp(&b).is_eq(),
924        (None, None) => true,
925        _ => false,
926    }
927}
928
929impl Annotations {
930    /// Creates annotations with no audience, priority, or timestamp hints set.
931    #[must_use]
932    pub fn new() -> Self {
933        Self::default()
934    }
935
936    /// Sets or clears the optional `audience` field.
937    #[must_use]
938    pub fn audience(mut self, audience: impl IntoOption<Vec<Role>>) -> Self {
939        self.audience = audience.into_option();
940        self
941    }
942
943    /// Sets or clears the optional `lastModified` field.
944    #[must_use]
945    pub fn last_modified(mut self, last_modified: impl IntoOption<String>) -> Self {
946        self.last_modified = last_modified.into_option();
947        self
948    }
949
950    /// Sets or clears the optional `priority` field.
951    #[must_use]
952    pub fn priority(mut self, priority: impl IntoOption<f64>) -> Self {
953        self.priority = priority.into_option();
954        self
955    }
956
957    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
958    /// metadata to their interactions. Implementations MUST NOT assign meaning to keys they
959    /// do not recognize.
960    ///
961    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
962    #[must_use]
963    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
964        self.meta = meta.into_option();
965        self
966    }
967}
968
969/// The sender or recipient of messages and data in a conversation.
970#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
971#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
972#[serde(rename_all = "camelCase")]
973#[non_exhaustive]
974pub enum Role {
975    /// The assistant side of a conversation.
976    Assistant,
977    /// The user side of a conversation.
978    User,
979    /// Custom or future role.
980    ///
981    /// Values beginning with `_` are reserved for implementation-specific
982    /// extensions. Unknown values that do not begin with `_` are reserved for
983    /// future ACP variants.
984    #[serde(untagged)]
985    Other(String),
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991
992    #[test]
993    fn test_text_content_roundtrip() {
994        let content = TextContent::new("hello world");
995        let json = serde_json::to_value(&content).unwrap();
996        let parsed: TextContent = serde_json::from_value(json).unwrap();
997        assert_eq!(content, parsed);
998    }
999
1000    #[test]
1001    fn test_annotations_equality_is_reflexive() {
1002        let nan = Annotations::new().priority(f64::NAN);
1003        assert_eq!(nan, nan.clone());
1004        assert_eq!(
1005            Annotations::new().priority(0.5),
1006            Annotations::new().priority(0.5)
1007        );
1008        assert_ne!(
1009            Annotations::new().priority(0.0),
1010            Annotations::new().priority(-0.0)
1011        );
1012        assert_ne!(
1013            serde_json::to_string(&Annotations::new().priority(0.0)).unwrap(),
1014            serde_json::to_string(&Annotations::new().priority(-0.0)).unwrap()
1015        );
1016        assert_ne!(
1017            Annotations::new().priority(0.5),
1018            Annotations::new().priority(1.0)
1019        );
1020        assert_ne!(nan, Annotations::new().priority(0.5));
1021        assert_ne!(Annotations::new().priority(0.5), Annotations::new());
1022        assert_ne!(
1023            Annotations::new().priority(0.5).audience(vec![Role::User]),
1024            Annotations::new().priority(0.5)
1025        );
1026    }
1027
1028    #[test]
1029    fn test_text_content_omits_optional_fields() {
1030        let content = TextContent::new("hello");
1031        let json = serde_json::to_value(&content).unwrap();
1032        assert!(!json.as_object().unwrap().contains_key("annotations"));
1033        assert!(!json.as_object().unwrap().contains_key("meta"));
1034    }
1035
1036    #[test]
1037    fn test_text_content_meta_defaults_on_missing_or_malformed_value() {
1038        let missing: TextContent = serde_json::from_value(serde_json::json!({
1039            "text": "hello"
1040        }))
1041        .unwrap();
1042        assert_eq!(missing.meta, None);
1043
1044        let malformed: TextContent = serde_json::from_value(serde_json::json!({
1045            "text": "hello",
1046            "_meta": false
1047        }))
1048        .unwrap();
1049        assert_eq!(malformed.meta, None);
1050    }
1051
1052    #[test]
1053    fn test_text_content_from_string() {
1054        let block: ContentBlock = "hello".into();
1055        match block {
1056            ContentBlock::Text(c) => assert_eq!(c.text, "hello"),
1057            _ => panic!("Expected Text variant"),
1058        }
1059    }
1060
1061    #[test]
1062    fn role_preserves_unknown_variant() {
1063        let role: Role = serde_json::from_str("\"critic\"").unwrap();
1064        assert_eq!(role, Role::Other("critic".to_string()));
1065        assert_eq!(serde_json::to_value(&role).unwrap(), "critic");
1066    }
1067
1068    #[test]
1069    fn icon_theme_preserves_unknown_variant() {
1070        let theme: IconTheme = serde_json::from_str("\"contrast\"").unwrap();
1071        assert_eq!(theme, IconTheme::Other("contrast".to_string()));
1072        assert_eq!(serde_json::to_value(&theme).unwrap(), "contrast");
1073    }
1074
1075    #[test]
1076    fn content_block_preserves_unknown_variant() {
1077        let block: ContentBlock = serde_json::from_value(serde_json::json!({
1078            "type": "_widget",
1079            "title": "Status",
1080            "state": {"ok": true}
1081        }))
1082        .unwrap();
1083
1084        let ContentBlock::Other(unknown) = block else {
1085            panic!("expected unknown content block");
1086        };
1087
1088        assert_eq!(unknown.type_, "_widget");
1089        assert_eq!(
1090            unknown.fields.get("title"),
1091            Some(&serde_json::json!("Status"))
1092        );
1093        assert_eq!(
1094            serde_json::to_value(ContentBlock::Other(unknown)).unwrap(),
1095            serde_json::json!({
1096                "type": "_widget",
1097                "title": "Status",
1098                "state": {"ok": true}
1099            })
1100        );
1101    }
1102
1103    #[test]
1104    fn content_block_does_not_hide_malformed_known_variant() {
1105        assert!(
1106            serde_json::from_value::<ContentBlock>(serde_json::json!({
1107                "type": "text"
1108            }))
1109            .is_err()
1110        );
1111    }
1112
1113    #[test]
1114    fn test_image_content_roundtrip() {
1115        let content = ImageContent::new("base64data", "image/png");
1116        let json = serde_json::to_value(&content).unwrap();
1117        let parsed: ImageContent = serde_json::from_value(json).unwrap();
1118        assert_eq!(content, parsed);
1119    }
1120
1121    #[test]
1122    fn test_image_content_omits_optional_fields() {
1123        let content = ImageContent::new("data", "image/png");
1124        let json = serde_json::to_value(&content).unwrap();
1125        assert!(!json.as_object().unwrap().contains_key("uri"));
1126        assert!(!json.as_object().unwrap().contains_key("annotations"));
1127        assert!(!json.as_object().unwrap().contains_key("meta"));
1128    }
1129
1130    #[test]
1131    fn test_image_content_with_uri() {
1132        let content = ImageContent::new("data", "image/png").uri("https://example.com/image.png");
1133        let json = serde_json::to_value(&content).unwrap();
1134        assert_eq!(json["uri"], "https://example.com/image.png");
1135    }
1136
1137    #[test]
1138    fn test_audio_content_roundtrip() {
1139        let content = AudioContent::new("base64audio", "audio/mp3");
1140        let json = serde_json::to_value(&content).unwrap();
1141        let parsed: AudioContent = serde_json::from_value(json).unwrap();
1142        assert_eq!(content, parsed);
1143    }
1144
1145    #[test]
1146    fn test_audio_content_omits_optional_fields() {
1147        let content = AudioContent::new("data", "audio/mp3");
1148        let json = serde_json::to_value(&content).unwrap();
1149        assert!(!json.as_object().unwrap().contains_key("annotations"));
1150        assert!(!json.as_object().unwrap().contains_key("meta"));
1151    }
1152
1153    #[test]
1154    fn resource_link_icons_roundtrip() {
1155        let icon = Icon::new("https://example.com/icon.png")
1156            .mime_type("image/png")
1157            .sizes(vec!["48x48".to_string(), "any".to_string()])
1158            .theme(IconTheme::Dark);
1159        let link = ResourceLink::new("Example", "file:///example.txt").icons(vec![icon]);
1160
1161        let json = serde_json::to_value(&link).unwrap();
1162        assert_eq!(json["icons"][0]["src"], "https://example.com/icon.png");
1163        assert_eq!(json["icons"][0]["mimeType"], "image/png");
1164        assert_eq!(json["icons"][0]["sizes"][0], "48x48");
1165        assert_eq!(json["icons"][0]["theme"], "dark");
1166
1167        let parsed: ResourceLink = serde_json::from_value(json).unwrap();
1168        assert_eq!(link, parsed);
1169    }
1170
1171    #[cfg(feature = "schemars")]
1172    #[test]
1173    fn annotations_priority_schema_matches_mcp_bounds() {
1174        let schema = schemars::schema_for!(Annotations);
1175        let json = serde_json::to_value(schema).unwrap();
1176
1177        assert_eq!(json["properties"]["priority"]["minimum"], 0);
1178        assert_eq!(json["properties"]["priority"]["maximum"], 1);
1179        // MCP allows any ISO 8601 string, so ACP does not narrow it to `date-time`.
1180        assert!(json["properties"]["lastModified"].get("format").is_none());
1181    }
1182
1183    #[cfg(feature = "schemars")]
1184    #[test]
1185    fn content_schema_uses_standard_string_annotations() {
1186        let image = serde_json::to_value(schemars::schema_for!(ImageContent)).unwrap();
1187        assert_eq!(image["properties"]["data"]["contentEncoding"], "base64");
1188        assert!(image["properties"]["data"].get("format").is_none());
1189        assert_eq!(image["properties"]["uri"]["format"], "uri");
1190
1191        let audio = serde_json::to_value(schemars::schema_for!(AudioContent)).unwrap();
1192        assert_eq!(audio["properties"]["data"]["contentEncoding"], "base64");
1193        assert!(audio["properties"]["data"].get("format").is_none());
1194
1195        let text_resource =
1196            serde_json::to_value(schemars::schema_for!(TextResourceContents)).unwrap();
1197        assert_eq!(text_resource["properties"]["uri"]["format"], "uri");
1198
1199        let blob_resource =
1200            serde_json::to_value(schemars::schema_for!(BlobResourceContents)).unwrap();
1201        assert_eq!(
1202            blob_resource["properties"]["blob"]["contentEncoding"],
1203            "base64"
1204        );
1205        assert!(blob_resource["properties"]["blob"].get("format").is_none());
1206        assert_eq!(blob_resource["properties"]["uri"]["format"], "uri");
1207
1208        let resource_link = serde_json::to_value(schemars::schema_for!(ResourceLink)).unwrap();
1209        assert_eq!(resource_link["properties"]["uri"]["format"], "uri");
1210
1211        let icon = serde_json::to_value(schemars::schema_for!(Icon)).unwrap();
1212        assert_eq!(icon["properties"]["src"]["format"], "uri");
1213        assert_eq!(icon["properties"]["sizes"]["items"]["type"], "string");
1214        assert!(
1215            icon["properties"]["sizes"]["items"]
1216                .get("pattern")
1217                .is_none()
1218        );
1219    }
1220}