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
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ for _, item := range response.Output {
```go
import (
"encoding/json"
"fmt"
"github.com/invopop/jsonschema"
// ...
)
Expand All @@ -281,16 +282,30 @@ type Origin struct {
Organization string `json:"organization" jsonschema_description:"The organization that was in charge of its development"`
}

// Structured Outputs uses a subset of JSON schema
// These flags are necessary to comply with the subset
func GenerateSchema[T any]() (map[string]any, error) {
// Structured Outputs requires object schemas to disallow additional
// properties. Keep definitions referenced so recursive Go types terminate.
reflector := jsonschema.Reflector{
AllowAdditionalProperties: false,
DoNotReference: true,
}
var v T
schema := reflector.Reflect(v)

if schema.Ref != "" {
const definitionsPrefix = "#/$defs/"
if len(schema.Ref) <= len(definitionsPrefix) || schema.Ref[:len(definitionsPrefix)] != definitionsPrefix {
return nil, fmt.Errorf("expand root JSON schema reference %q: unsupported reference", schema.Ref)
}
definition, ok := schema.Definitions[schema.Ref[len(definitionsPrefix):]]
if !ok {
return nil, fmt.Errorf("expand root JSON schema reference %q: definition not found", schema.Ref)
}
expanded := *definition
expanded.Version = schema.Version
expanded.ID = schema.ID
expanded.Anchor = schema.Anchor
expanded.Definitions = schema.Definitions
schema = &expanded
}
data, err := json.Marshal(schema)
if err != nil {
return nil, err
Expand Down
21 changes: 18 additions & 3 deletions examples/structured-outputs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,29 @@ type Origin struct {
}

func GenerateSchema[T any]() (map[string]any, error) {
// Structured Outputs uses a subset of JSON schema
// These flags are necessary to comply with the subset
// Structured Outputs requires object schemas to disallow additional
// properties. Keep definitions referenced so recursive Go types terminate.
reflector := jsonschema.Reflector{
AllowAdditionalProperties: false,
DoNotReference: true,
}
var v T
schema := reflector.Reflect(v)
if schema.Ref != "" {
const definitionsPrefix = "#/$defs/"
if len(schema.Ref) <= len(definitionsPrefix) || schema.Ref[:len(definitionsPrefix)] != definitionsPrefix {
return nil, fmt.Errorf("expand root JSON schema reference %q: unsupported reference", schema.Ref)
}
definition, ok := schema.Definitions[schema.Ref[len(definitionsPrefix):]]
if !ok {
return nil, fmt.Errorf("expand root JSON schema reference %q: definition not found", schema.Ref)
}
expanded := *definition
expanded.Version = schema.Version
expanded.ID = schema.ID
expanded.Anchor = schema.Anchor
expanded.Definitions = schema.Definitions
schema = &expanded
}
data, err := json.Marshal(schema)
if err != nil {
return nil, fmt.Errorf("marshal JSON schema: %w", err)
Expand Down
55 changes: 55 additions & 0 deletions examples/structured-outputs/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"strings"
"testing"
)

Expand Down Expand Up @@ -101,3 +102,57 @@ func TestGenerateSchemaPreserves64BitIntegerEnums(t *testing.T) {
}
}
}

func TestGenerateSchemaPreservesRecursiveReferences(t *testing.T) {
type recursiveSchemaNode struct {
Children []recursiveSchemaNode `json:"children"`
}

schema, err := GenerateSchema[recursiveSchemaNode]()
if err != nil {
t.Fatalf("GenerateSchema() error = %v", err)
}

data, err := json.Marshal(schema)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}

var decoded struct {
Type string `json:"type"`
Defs map[string]struct {
Properties map[string]struct {
Items struct {
Ref string `json:"$ref"`
} `json:"items"`
} `json:"properties"`
} `json:"$defs"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}

if decoded.Type != "object" {
t.Fatalf("root type = %q, want %q", decoded.Type, "object")
}
if len(decoded.Defs) == 0 {
t.Fatal("recursive schema has no $defs")
}

const refPrefix = "#/$defs/"
for definitionName, definition := range decoded.Defs {
children, ok := definition.Properties["children"]
if !ok {
continue
}
if !strings.HasPrefix(children.Items.Ref, refPrefix) {
t.Fatalf("%s children items $ref = %q, want prefix %q", definitionName, children.Items.Ref, refPrefix)
}
if strings.TrimPrefix(children.Items.Ref, refPrefix) != definitionName {
t.Fatalf("%s children items $ref = %q, want self-reference", definitionName, children.Items.Ref)
}
return
}

t.Fatalf("recursive $defs has no definition with children property: %s", data)
}