-
Notifications
You must be signed in to change notification settings - Fork 46
fix: generate openAI-compatible json schemas for list types #272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b437692
04cee88
ee83b8c
a0efb1e
3f2c5f6
fca294f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ### fix: generate openAI-compatible json schemas for list types - @DaleSeo PR #272 | ||
|
|
||
| The MCP server is generating JSON schemas that don't match OpenAI's function calling specification. It puts `oneOf` at the array level instead of using `items` to define the JSON schemas for the GraphQL list types. While some other LLMs are more flexible about this, it technically violates the [JSON Schema specification](https://json-schema.org/understanding-json-schema/reference/array) that OpenAI strictly follows. | ||
|
|
||
| This PR updates the list type handling logic to move `oneOf` inside `items` for GraphQL list types. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1044,25 +1044,36 @@ fn type_to_schema( | |
| custom_scalar_map, | ||
| definitions, | ||
| ); | ||
| let items_schema = if list_type.is_non_null() { | ||
| inner_type_schema | ||
| } else { | ||
| Schema::Object(SchemaObject { | ||
| subschemas: Some(Box::new(SubschemaValidation { | ||
| one_of: Some(vec![ | ||
| inner_type_schema, | ||
| Schema::Object(SchemaObject { | ||
| instance_type: Some(SingleOrVec::Single(Box::new( | ||
| InstanceType::Null, | ||
| ))), | ||
| ..Default::default() | ||
| }), | ||
| ]), | ||
| ..Default::default() | ||
| })), | ||
| ..Default::default() | ||
| }) | ||
| }; | ||
|
|
||
| schema_factory( | ||
| None, | ||
| Some(InstanceType::Array), | ||
| None, | ||
| list_type.is_non_null().then(|| ArrayValidation { | ||
| items: Some(SingleOrVec::Single(Box::new(inner_type_schema.clone()))), | ||
| ..Default::default() | ||
| }), | ||
| (!list_type.is_non_null()).then(|| SubschemaValidation { | ||
| one_of: Some(vec![ | ||
| inner_type_schema, | ||
| Schema::Object(SchemaObject { | ||
| instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), | ||
| ..Default::default() | ||
| }), | ||
| ]), | ||
| Some(ArrayValidation { | ||
| items: Some(SingleOrVec::Single(Box::new(items_schema))), | ||
| ..Default::default() | ||
| }), | ||
| None, | ||
| None, | ||
| ) | ||
| } | ||
| } | ||
|
|
@@ -1556,7 +1567,7 @@ mod tests { | |
| .unwrap(); | ||
| let tool = Tool::from(operation); | ||
|
|
||
| insta::assert_debug_snapshot!(tool, @r###" | ||
| insta::assert_debug_snapshot!(tool, @r#" | ||
| Tool { | ||
| name: "QueryName", | ||
| description: Some( | ||
|
|
@@ -1570,14 +1581,16 @@ mod tests { | |
| "properties": Object { | ||
| "id": Object { | ||
| "type": String("array"), | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("string"), | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| "items": Object { | ||
| "oneOf": Array [ | ||
| Object { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like all our array test currently only have scalars, could we have one that s a list of input objects? (one for nullable and one for non nullable)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added tests for input object lists. |
||
| "type": String("string"), | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -1593,8 +1606,8 @@ mod tests { | |
| }, | ||
| ), | ||
| } | ||
| "###); | ||
| insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" | ||
| "#); | ||
| insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" | ||
| { | ||
| "type": "object", | ||
| "required": [ | ||
|
|
@@ -1603,18 +1616,20 @@ mod tests { | |
| "properties": { | ||
| "id": { | ||
| "type": "array", | ||
| "oneOf": [ | ||
| { | ||
| "type": "string" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| "items": { | ||
| "oneOf": [ | ||
| { | ||
| "type": "string" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } | ||
| "###); | ||
| "#); | ||
|
Comment on lines
-1617
to
+1636
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not blocking, but what caused the change in all these snapshots to use the single
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test code was written before Insta v1.40.0. It used to use |
||
| } | ||
|
|
||
| #[test] | ||
|
|
@@ -1708,7 +1723,7 @@ mod tests { | |
| .unwrap(); | ||
| let tool = Tool::from(operation); | ||
|
|
||
| insta::assert_debug_snapshot!(tool, @r###" | ||
| insta::assert_debug_snapshot!(tool, @r#" | ||
| Tool { | ||
| name: "QueryName", | ||
| description: Some( | ||
|
|
@@ -1719,14 +1734,16 @@ mod tests { | |
| "properties": Object { | ||
| "id": Object { | ||
| "type": String("array"), | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("string"), | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| "items": Object { | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("string"), | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -1742,25 +1759,27 @@ mod tests { | |
| }, | ||
| ), | ||
| } | ||
| "###); | ||
| insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" | ||
| "#); | ||
| insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "id": { | ||
| "type": "array", | ||
| "oneOf": [ | ||
| { | ||
| "type": "string" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| "items": { | ||
| "oneOf": [ | ||
| { | ||
| "type": "string" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } | ||
| "###); | ||
| "#); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
@@ -1848,7 +1867,7 @@ mod tests { | |
| .unwrap(); | ||
| let tool = Tool::from(operation); | ||
|
|
||
| insta::assert_debug_snapshot!(tool, @r###" | ||
| insta::assert_debug_snapshot!(tool, @r#" | ||
| Tool { | ||
| name: "QueryName", | ||
| description: Some( | ||
|
|
@@ -1859,22 +1878,26 @@ mod tests { | |
| "properties": Object { | ||
| "id": Object { | ||
| "type": String("array"), | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("array"), | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("string"), | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| "items": Object { | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("array"), | ||
| "items": Object { | ||
| "oneOf": Array [ | ||
| Object { | ||
| "type": String("string"), | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| }, | ||
| Object { | ||
| "type": String("null"), | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -1890,33 +1913,37 @@ mod tests { | |
| }, | ||
| ), | ||
| } | ||
| "###); | ||
| insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" | ||
| "#); | ||
| insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "id": { | ||
| "type": "array", | ||
| "oneOf": [ | ||
| { | ||
| "type": "array", | ||
| "oneOf": [ | ||
| { | ||
| "type": "string" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| "items": { | ||
| "oneOf": [ | ||
| { | ||
| "type": "array", | ||
| "items": { | ||
| "oneOf": [ | ||
| { | ||
| "type": "string" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } | ||
| "###); | ||
| "#); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.