Skip to main content

llmprism/schema/
object.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::Schema;
5
6/// A schema for a JSON object made up of named fields -- this is what
7/// [`Tool::parameters`](crate::Tool::parameters) returns, and it's how you
8/// describe a whole tool's argument list.
9///
10/// Each entry in `properties` carries its own name (every [`Schema`] variant has
11/// one), so `properties` is a plain list rather than a name-keyed map -- you never
12/// have to keep a field's key and its schema's name in sync separately.
13///
14/// # Example
15///
16/// ```
17/// use llmprism::schema::{NumberSchema, ObjectSchema, Schema, StringSchema};
18///
19/// let parameters = ObjectSchema::new("parameters")
20///     .with_property(Schema::String(StringSchema::new("city")), true) // required
21///     .with_property(Schema::Number(NumberSchema::new("days_ahead")), false); // optional
22/// ```
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ObjectSchema {
25    /// The field/parameter name this schema describes (for a tool's top-level
26    /// parameter list, this is usually just a placeholder like `"parameters"`,
27    /// since it isn't itself nested inside another object).
28    pub name: String,
29    /// A plain-language explanation of what this object represents, shown to the
30    /// model so it can fill it in correctly.
31    pub description: Option<String>,
32    /// The object's fields. Each entry's own `name()` is its key.
33    pub properties: Vec<Schema>,
34    /// The names of fields the model must always provide (as opposed to fields
35    /// it may omit).
36    pub required: Vec<String>,
37    /// Whether the model is allowed to include fields beyond the ones listed in
38    /// `properties`. Defaults to `false` -- most providers expect a closed set of
39    /// fields.
40    pub allow_additional_properties: bool,
41    /// An already-formed JSON Schema document to send exactly as-is, bypassing
42    /// `properties`/`required`/`allow_additional_properties` entirely. `None`
43    /// (the default) builds the schema from those fields normally, the same
44    /// way it always has.
45    ///
46    /// This is [`RawSchema`](super::RawSchema)'s escape hatch, made available
47    /// here too: `RawSchema` works anywhere a general [`Schema`] fits, but
48    /// [`Tool::parameters`](crate::Tool::parameters) and
49    /// [`StructuredRequest::schema`](crate::structured::StructuredRequest::schema)
50    /// are both typed as `ObjectSchema` specifically, so `Schema::Raw` can't be
51    /// plugged in at either point. Set via
52    /// [`from_raw_json_schema`](Self::from_raw_json_schema) instead of directly,
53    /// for a schema you already have in hand -- from an MCP server's tool
54    /// listing, or a user-supplied schema file -- rather than one you're
55    /// building field by field.
56    pub json_schema: Option<Value>,
57}
58
59impl ObjectSchema {
60    /// Creates an empty object schema named `name`, with no fields yet. Add
61    /// fields with [`with_property`](Self::with_property).
62    pub fn new(name: impl Into<String>) -> Self {
63        Self {
64            name: name.into(),
65            description: None,
66            properties: Vec::new(),
67            required: Vec::new(),
68            allow_additional_properties: false,
69            json_schema: None,
70        }
71    }
72
73    /// Creates an object schema named `name` that sends `json_schema` exactly
74    /// as-is, instead of being built from [`with_property`](Self::with_property)
75    /// calls. See [`json_schema`](Self::json_schema) for when this is worth
76    /// reaching for.
77    ///
78    /// One caveat worth knowing if you're targeting OpenAI: its
79    /// structured-output request sets `"strict": true`, which requires
80    /// `additionalProperties: false` and every property listed in `required`,
81    /// recursively, throughout the whole schema. A raw schema that doesn't
82    /// already satisfy that shape will be rejected by OpenAI at request time --
83    /// this crate can't fix that up for you, since it sends `json_schema`
84    /// unmodified by design.
85    pub fn from_raw_json_schema(name: impl Into<String>, json_schema: Value) -> Self {
86        Self {
87            json_schema: Some(json_schema),
88            ..Self::new(name)
89        }
90    }
91
92    /// Attaches a plain-language description, shown to the model.
93    pub fn with_description(mut self, description: impl Into<String>) -> Self {
94        self.description = Some(description.into());
95        self
96    }
97
98    /// Adds one field to the object. `schema`'s own name becomes the field's key.
99    /// Set `required` to `true` if the model must always provide this field.
100    pub fn with_property(mut self, schema: Schema, required: bool) -> Self {
101        if required {
102            self.required.push(schema.name().to_string());
103        }
104        self.properties.push(schema);
105        self
106    }
107
108    /// Sets whether the model may include fields beyond the ones you've listed.
109    pub fn with_additional_properties(mut self, allow: bool) -> Self {
110        self.allow_additional_properties = allow;
111        self
112    }
113}