From 1303b75dd31df516e34695b3a08f13b073f8f598 Mon Sep 17 00:00:00 2001 From: Scott Ames-Messinger Date: Fri, 21 Mar 2025 09:32:35 -0400 Subject: [PATCH] feat: fix JSON Schema error with embeds and title field OpenAI throws an error if the embeds_one relation has ` title` field in it. It can only have a `$ref`. Removing the title field required changing how embeds are handled. Embeds are now handled not like fields but like associations. logs clean up --- lib/instructor/json_schema.ex | 28 ++++++++++++- test/json_schema_test.exs | 75 +++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/lib/instructor/json_schema.ex b/lib/instructor/json_schema.ex index d2419bb..7d8fddd 100644 --- a/lib/instructor/json_schema.ex +++ b/lib/instructor/json_schema.ex @@ -133,6 +133,7 @@ defmodule Instructor.JSONSchema do properties = ecto_schema.__schema__(:fields) + |> Enum.reject(fn field -> field in ecto_schema.__schema__(:embeds) end) |> Enum.map(fn field -> type = ecto_schema.__schema__(:type, field) value = for_type(type) @@ -165,7 +166,29 @@ defmodule Instructor.JSONSchema do end) |> Enum.into(%{}) - properties = Map.merge(properties, associations) + embeds = + ecto_schema.__schema__(:embeds) + |> Enum.map(&ecto_schema.__schema__(:embed, &1)) + |> Enum.map(fn association -> + field = association.field + title = title_for(association.related) + + value = + if association.cardinality == :many do + %{ + items: %{"$ref": "#/$defs/#{title}"}, + title: title, + type: "array" + } + else + %{"$ref": "#/$defs/#{title}"} + end + + {field, value} + end) + |> Enum.into(%{}) + + properties = properties |> Map.merge(associations) |> Map.merge(embeds) required = Map.keys(properties) |> Enum.sort() title = title_for(ecto_schema) @@ -447,7 +470,8 @@ defmodule Instructor.JSONSchema do |> maybe_call_with_path(fun, path, opts) end - defp do_traverse_and_update(tree, fun, path, opts), do: maybe_call_with_path(tree, fun, path, opts) + defp do_traverse_and_update(tree, fun, path, opts), + do: maybe_call_with_path(tree, fun, path, opts) defp maybe_call_with_path(value, fun, path, opts) do if Keyword.get(opts, :include_path, false) do diff --git a/test/json_schema_test.exs b/test/json_schema_test.exs index b8b8ab8..009f510 100644 --- a/test/json_schema_test.exs +++ b/test/json_schema_test.exs @@ -299,6 +299,18 @@ defmodule JSONSchemaTest do @primary_key false embedded_schema do field(:string, :string) + + embeds_many :many, Many do + field(:string, :string) + + embeds_one :one, One do + field(:string, :string) + + embeds_many :many, Many do + field(:string, :string) + end + end + end end end @@ -324,19 +336,76 @@ defmodule JSONSchemaTest do "title" => "string", "type" => "string", "description" => "String, e.g. 'hello'" + }, + "many" => %{ + "items" => %{"$ref" => "#/$defs/JSONSchemaTest.Embedded.Many"}, + "title" => "JSONSchemaTest.Embedded.Many", + "type" => "array" } }, - "required" => ["string"], + "required" => ["many", "string"], "title" => "JSONSchemaTest.Embedded", "type" => "object", "additionalProperties" => false + }, + "JSONSchemaTest.Embedded.Many" => %{ + "title" => "JSONSchemaTest.Embedded.Many", + "type" => "object", + "required" => ["id", "one", "string"], + "additionalProperties" => false, + "description" => "", + "properties" => %{ + "string" => %{ + "title" => "string", + "type" => "string", + "description" => "String, e.g. 'hello'" + }, + "id" => %{"title" => "id", "type" => "string"}, + "one" => %{ + "$ref" => "#/$defs/JSONSchemaTest.Embedded.Many.One" + } + } + }, + "JSONSchemaTest.Embedded.Many.One" => %{ + "title" => "JSONSchemaTest.Embedded.Many.One", + "type" => "object", + "required" => ["id", "many", "string"], + "additionalProperties" => false, + "description" => "", + "properties" => %{ + "id" => %{"title" => "id", "type" => "string"}, + "string" => %{ + "title" => "string", + "type" => "string", + "description" => "String, e.g. 'hello'" + }, + "many" => %{ + "items" => %{"$ref" => "#/$defs/JSONSchemaTest.Embedded.Many.One.Many"}, + "title" => "JSONSchemaTest.Embedded.Many.One.Many", + "type" => "array" + } + } + }, + "JSONSchemaTest.Embedded.Many.One.Many" => %{ + "title" => "JSONSchemaTest.Embedded.Many.One.Many", + "type" => "object", + "required" => ["id", "string"], + "additionalProperties" => false, + "description" => "", + "properties" => %{ + "id" => %{"title" => "id", "type" => "string"}, + "string" => %{ + "title" => "string", + "type" => "string", + "description" => "String, e.g. 'hello'" + } + } } }, "description" => "", "properties" => %{ "embedded" => %{ - "$ref" => "#/$defs/JSONSchemaTest.Embedded", - "title" => "embedded" + "$ref" => "#/$defs/JSONSchemaTest.Embedded" } }, "required" => ["embedded"],