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}