Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions typify-impl/src/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,14 +382,20 @@ fn try_merge_with_each_subschema(
.iter()
.enumerate()
.filter(|(jj, _)| *jj != ii)
.map(|(_, not_schema)| {
Schema::Object(SchemaObject {
.filter_map(|(_, not_schema)| {
// A disjoint branch is already excluded by the merged
// schema. Subtracting it again can lose information
// when a shared property accepts any value.
if try_merge_schema(&merged_schema, not_schema, defs).is_err() {
return None;
}
Some(Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
not: Some(Box::new(not_schema.clone())),
..Default::default()
})),
..Default::default()
})
}))
});
let joined_schema = [schema.clone(), other.clone()]
.into_iter()
Expand Down Expand Up @@ -1335,6 +1341,61 @@ mod tests {

use super::try_merge_schema;

#[test]
fn disjoint_oneof_branches_keep_shared_and_variant_properties() {
let input: schemars::schema::Schema = serde_json::from_value(json!({
"type": "object",
"properties": {"serverName": {"type": "string"}},
"required": ["serverName"],
"oneOf": [
{
"type": "object",
"properties": {
"mode": {"enum": ["form"]},
"requestedSchema": {"type": "object"},
"_meta": true
},
"required": ["mode", "requestedSchema"]
},
{
"type": "object",
"properties": {
"mode": {"enum": ["openai/form"]},
"requestedSchema": true,
"_meta": true
},
"required": ["mode", "requestedSchema"]
}
]
}))
.unwrap();

let object = input.into_object();
let merged = super::try_merge_with_subschemas(
schemars::schema::SchemaObject {
subschemas: None,
..object.clone()
},
object.subschemas.as_deref(),
&BTreeMap::new(),
)
.unwrap();
let branches = merged.subschemas.unwrap().one_of.unwrap();
assert_eq!(branches.len(), 2);
for branch in branches {
let branch = branch.into_object();
let parts = branch.subschemas.unwrap().all_of.unwrap();
let properties = super::merge_all(&parts, &BTreeMap::new())
.into_object()
.object
.unwrap()
.properties;
for key in ["serverName", "mode", "requestedSchema", "_meta"] {
assert!(properties.contains_key(key), "missing {key}");
}
}
}

#[test]
fn test_simple_merge() {
let a = json!({
Expand Down
34 changes: 34 additions & 0 deletions typify/tests/schemas/disjoint-oneof-shared-properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Elicitation": {
"title": "Elicitation",
"type": "object",
"properties": {
"serverName": { "type": "string" },
"threadId": { "type": "string" }
},
"required": ["serverName", "threadId"],
"oneOf": [
{
"type": "object",
"properties": {
"mode": { "type": "string", "enum": ["form"] },
"requestedSchema": { "type": "object" },
"_meta": true
},
"required": ["mode", "requestedSchema"]
},
{
"type": "object",
"properties": {
"mode": { "type": "string", "enum": ["openai/form"] },
"requestedSchema": true,
"_meta": true
},
"required": ["mode", "requestedSchema"]
}
]
}
}
}
155 changes: 155 additions & 0 deletions typify/tests/schemas/disjoint-oneof-shared-properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#![deny(warnings)]
#[doc = "`Elicitation`"]
#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)]
#[serde(untagged)]
pub enum Elicitation {
Variant0 {
#[serde(
rename = "_meta",
skip_serializing_if = "::std::option::Option::is_none"
)]
meta: ::std::option::Option<::serde_json::Value>,
mode: ElicitationVariant0Mode,
#[serde(rename = "requestedSchema")]
requested_schema: ::serde_json::Map<::std::string::String, ::serde_json::Value>,
#[serde(rename = "serverName")]
server_name: ::std::string::String,
#[serde(rename = "threadId")]
thread_id: ::std::string::String,
},
Variant1 {
#[serde(
rename = "_meta",
skip_serializing_if = "::std::option::Option::is_none"
)]
meta: ::std::option::Option<::serde_json::Value>,
mode: ElicitationVariant1Mode,
#[serde(rename = "requestedSchema")]
requested_schema: ::serde_json::Value,
#[serde(rename = "serverName")]
server_name: ::std::string::String,
#[serde(rename = "threadId")]
thread_id: ::std::string::String,
},
}
#[doc = "`ElicitationVariant0Mode`"]
#[derive(
:: serde :: Deserialize,
:: serde :: Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
pub enum ElicitationVariant0Mode {
#[serde(rename = "form")]
Form,
}
impl ::std::fmt::Display for ElicitationVariant0Mode {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Form => f.write_str("form"),
}
}
}
impl ::std::str::FromStr for ElicitationVariant0Mode {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"form" => Ok(Self::Form),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ElicitationVariant0Mode {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ElicitationVariant0Mode {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
#[doc = "`ElicitationVariant1Mode`"]
#[derive(
:: serde :: Deserialize,
:: serde :: Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
pub enum ElicitationVariant1Mode {
#[serde(rename = "openai/form")]
OpenaiForm,
}
impl ::std::fmt::Display for ElicitationVariant1Mode {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::OpenaiForm => f.write_str("openai/form"),
}
}
}
impl ::std::str::FromStr for ElicitationVariant1Mode {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"openai/form" => Ok(Self::OpenaiForm),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ElicitationVariant1Mode {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ElicitationVariant1Mode {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
#[doc = " Error types."]
pub mod error {
#[doc = r" Error from a `TryFrom` or `FromStr` implementation."]
pub struct ConversionError(::std::borrow::Cow<'static, str>);
impl ::std::error::Error for ConversionError {}
impl ::std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl ::std::fmt::Debug for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Debug::fmt(&self.0, f)
}
}
impl From<&'static str> for ConversionError {
fn from(value: &'static str) -> Self {
Self(value.into())
}
}
impl From<String> for ConversionError {
fn from(value: String) -> Self {
Self(value.into())
}
}
}
fn main() {}