-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Improve handling of JSON Schema in OpenAI API Response Context #819
Changes from 8 commits
47856de
0e89407
545ee4b
8bb6204
051a17f
8b49a36
cdb0075
1bf03a8
d3fd653
162bb6a
35d36ca
a80ea2f
5018f63
290bc29
25d8769
e21015f
9332d59
a4c1156
4d0750d
9db4d84
8680e7b
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 |
---|---|---|
|
@@ -4,7 +4,12 @@ | |
// and/or pass in the schema in []byte format. | ||
package jsonschema | ||
|
||
import "encoding/json" | ||
import ( | ||
"encoding/json" | ||
"reflect" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
type DataType string | ||
|
||
|
@@ -53,3 +58,90 @@ | |
Alias: (Alias)(d), | ||
}) | ||
} | ||
|
||
type SchemaWrapper[T any] struct { | ||
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. Pardon my ignorance: do we even need this type? 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. The sw, err := jsonschema.Wrap(MyStructuredResponse{})
result, err := sw.Unmarshal(`{...}`) or var result MyStructuredResponse{}
schema, err := jsonschema.GenerateSchemaForType(result)
schema.Unmarshal(`{...}`, &result) |
||
data T | ||
schema Definition | ||
} | ||
|
||
func (r SchemaWrapper[T]) Schema() Definition { | ||
return r.schema | ||
} | ||
|
||
func (r SchemaWrapper[T]) MarshalJSON() ([]byte, error) { | ||
return json.Marshal(r.schema) | ||
} | ||
|
||
func (r SchemaWrapper[T]) Unmarshal(content string) (*T, error) { | ||
var v T | ||
err := Unmarshal(r.schema, []byte(content), &v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &v, nil | ||
} | ||
|
||
func (r SchemaWrapper[T]) String() string { | ||
bytes, _ := json.MarshalIndent(r.schema, "", " ") | ||
return string(bytes) | ||
} | ||
|
||
func Warp[T any](v T) SchemaWrapper[T] { | ||
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. @eiixy heads up: I think you meant 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. Or even better: |
||
return SchemaWrapper[T]{ | ||
data: v, | ||
schema: reflectSchema(reflect.TypeOf(v)), | ||
} | ||
} | ||
|
||
func reflectSchema(t reflect.Type) Definition { | ||
var d Definition | ||
switch t.Kind() { | ||
Check failure on line 98 in jsonschema/json.go GitHub Actions / Sanity check
|
||
case reflect.String: | ||
d.Type = String | ||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, | ||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | ||
d.Type = Integer | ||
case reflect.Float32, reflect.Float64: | ||
d.Type = Number | ||
case reflect.Bool: | ||
d.Type = Boolean | ||
case reflect.Slice, reflect.Array: | ||
d.Type = Array | ||
items := reflectSchema(t.Elem()) | ||
d.Items = &items | ||
case reflect.Struct: | ||
d.Type = Object | ||
d.AdditionalProperties = false | ||
properties := make(map[string]Definition) | ||
var requiredFields []string | ||
for i := 0; i < t.NumField(); i++ { | ||
field := t.Field(i) | ||
jsonTag := field.Tag.Get("json") | ||
var required = true | ||
if jsonTag == "" { | ||
jsonTag = field.Name | ||
} else if strings.HasSuffix(jsonTag, ",omitempty") { | ||
jsonTag = strings.TrimSuffix(jsonTag, ",omitempty") | ||
required = false | ||
} | ||
|
||
item := reflectSchema(field.Type) | ||
description := field.Tag.Get("description") | ||
if description != "" { | ||
item.Description = description | ||
} | ||
properties[jsonTag] = item | ||
|
||
if s := field.Tag.Get("required"); s != "" { | ||
required, _ = strconv.ParseBool(s) | ||
} | ||
if required { | ||
requiredFields = append(requiredFields, jsonTag) | ||
} | ||
} | ||
d.Required = requiredFields | ||
d.Properties = properties | ||
default: | ||
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 suggest we either return an error or panic here 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. Thank you for the suggestion! |
||
} | ||
return d | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package jsonschema | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
) | ||
|
||
func Unmarshal(schema Definition, content []byte, v any) error { | ||
eiixy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var data any | ||
err := json.Unmarshal(content, &data) | ||
if err != nil { | ||
return err | ||
} | ||
if !Validate(schema, data) { | ||
return errors.New("validate failed") | ||
} | ||
return json.Unmarshal(content, &v) | ||
} | ||
|
||
func Validate(schema Definition, data interface{}) bool { | ||
eiixy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
switch schema.Type { | ||
case Object: | ||
return validateObject(schema, data) | ||
case Array: | ||
return validateArray(schema, data) | ||
case String: | ||
_, ok := data.(string) | ||
return ok | ||
case Number: // float64 and int | ||
_, ok := data.(float64) | ||
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. should we also support float32 here? |
||
if !ok { | ||
_, ok = data.(int) | ||
} | ||
return ok | ||
case Boolean: | ||
_, ok := data.(bool) | ||
return ok | ||
case Integer: | ||
_, ok := data.(int) | ||
return ok | ||
case Null: | ||
return data == nil | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
func validateObject(schema Definition, data any) bool { | ||
dataMap, ok := data.(map[string]any) | ||
if !ok { | ||
return false | ||
} | ||
for _, field := range schema.Required { | ||
if _, exists := dataMap[field]; !exists { | ||
return false | ||
} | ||
} | ||
for key, valueSchema := range schema.Properties { | ||
value, exists := dataMap[key] | ||
if exists && !Validate(valueSchema, value) { | ||
return false | ||
} else if !exists && contains(schema.Required, key) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func validateArray(schema Definition, data any) bool { | ||
dataArray, ok := data.([]interface{}) | ||
eiixy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if !ok { | ||
return false | ||
} | ||
for _, item := range dataArray { | ||
if !Validate(*schema.Items, item) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func contains[S ~[]E, E comparable](s S, v E) bool { | ||
for i := range s { | ||
if v == s[i] { | ||
return true | ||
} | ||
} | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great stuff, I guess we should put a proper README example on how to use this. That should be one of the top examples for sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added an example use case from OpenAI documentation to the README. For reference, please see: https://platform.openai.com/docs/guides/structured-outputs/examples