Skip to content
Merged
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
264 changes: 264 additions & 0 deletions _testdata/positive/array_element_discrimination.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
{
"openapi": "3.0.3",
"info": {
"title": "Array Element Type Discrimination Test",
"version": "1.0.0",
"description": "Tests discrimination between oneOf variants based on array element types (currently unsupported but should work)"
},
"paths": {
"/basic-arrays": {
"get": {
"operationId": "getBasicArrays",
"description": "Test basic array element discrimination: string[] vs integer[]",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BasicArrayResource"
}
}
}
}
}
}
}
},
"/object-vs-primitive-arrays": {
"get": {
"operationId": "getObjectVsPrimitiveArrays",
"description": "Test array of objects vs array of primitives",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ObjectVsPrimitiveResource"
}
}
}
}
}
}
}
},
"/mixed-discrimination": {
"post": {
"operationId": "postMixedDiscrimination",
"description": "Test mixed discrimination with array field + other fields",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MixedDiscriminationResource"
}
}
}
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"components": {
"schemas": {
"BasicArrayResource": {
"oneOf": [
{
"$ref": "#/components/schemas/StringArrayVariant"
},
{
"$ref": "#/components/schemas/IntegerArrayVariant"
},
{
"$ref": "#/components/schemas/BooleanArrayVariant"
}
]
},
"StringArrayVariant": {
"type": "object",
"required": ["name", "items"],
"properties": {
"name": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of string items",
"items": {
"type": "string"
}
},
"stringInfo": {
"type": "string",
"description": "Unique to string variant for fallback discrimination"
}
}
},
"IntegerArrayVariant": {
"type": "object",
"required": ["name", "items"],
"properties": {
"name": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of integer items",
"items": {
"type": "integer"
}
},
"intInfo": {
"type": "integer",
"description": "Unique to integer variant for fallback discrimination"
}
}
},
"BooleanArrayVariant": {
"type": "object",
"required": ["name", "items"],
"properties": {
"name": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of boolean items",
"items": {
"type": "boolean"
}
},
"boolInfo": {
"type": "boolean",
"description": "Unique to boolean variant for fallback discrimination"
}
}
},
"ObjectVsPrimitiveResource": {
"oneOf": [
{
"$ref": "#/components/schemas/ObjectArrayVariant"
},
{
"$ref": "#/components/schemas/PrimitiveArrayVariant"
}
]
},
"ObjectArrayVariant": {
"type": "object",
"required": ["id", "items"],
"properties": {
"id": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of object items",
"items": {
"type": "object",
"required": ["key", "value"],
"properties": {
"key": {
"type": "string"
},
"value": {
"type": "integer"
}
}
}
},
"objectMeta": {
"type": "object",
"description": "Unique to object variant for fallback discrimination",
"properties": {
"version": {
"type": "integer"
}
}
}
}
},
"PrimitiveArrayVariant": {
"type": "object",
"required": ["id", "items"],
"properties": {
"id": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of primitive string items",
"items": {
"type": "string"
}
},
"primitiveMeta": {
"type": "string",
"description": "Unique to primitive variant for fallback discrimination"
}
}
},
"MixedDiscriminationResource": {
"oneOf": [
{
"$ref": "#/components/schemas/MixedStringArrayVariant"
},
{
"$ref": "#/components/schemas/MixedNumberArrayVariant"
}
]
},
"MixedStringArrayVariant": {
"type": "object",
"required": ["id", "items", "metadata"],
"properties": {
"id": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of strings",
"items": {
"type": "string"
}
},
"metadata": {
"type": "string",
"description": "Unique to string variant"
}
}
},
"MixedNumberArrayVariant": {
"type": "object",
"required": ["id", "items", "count"],
"properties": {
"id": {
"type": "string"
},
"items": {
"type": "array",
"description": "Array of numbers",
"items": {
"type": "number"
}
},
"count": {
"type": "integer",
"description": "Unique to number variant"
}
}
}
}
}
}
40 changes: 40 additions & 0 deletions gen/_template/json/encoders_sum.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,46 @@ func (s *{{ $.Name }}) Decode(d *jx.Decoder) error {
}
found = true
s.Type = match
{{- else if needsArrayElementDiscrimination $variants }}
// Array element discrimination: peek into array to check first element type
if typ := d.Next(); typ != jx.Array {
return d.Skip()
}
// Capture array to peek at first element without consuming
if err := d.Capture(func(d *jx.Decoder) error {
// Check if array is empty
iter, err := d.ArrIter()
if err != nil {
return err
}
if !iter.Next() {
// Empty array - use first variant as default
{{- $firstVariant := index (dedupeVariantsByArrayElementType $variants) 0 }}
if !found {
found = true
s.Type = {{ $firstVariant.VariantType }}
}
return nil
}
elemType := d.Next()
switch elemType {
{{- range $v := dedupeVariantsByArrayElementType $variants }}
{{- if $v.ArrayElementType }}
case {{ $v.ArrayElementType }}:
match := {{ $v.VariantType }}
if found && s.Type != match {
s.Type = ""
return errors.Errorf("multiple oneOf matches: (%v, %v)", s.Type, match)
}
found = true
s.Type = match
{{- end }}
{{- end }}
}
return nil
}); err != nil {
return err
}
{{- else }}
// Multiple variants have this field - use type checking to discriminate
typ := d.Next()
Expand Down
9 changes: 9 additions & 0 deletions gen/ir/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ type UniqueFieldVariant struct {
VariantType string // e.g., "SystemEventEvent"
FieldType string // jx.Type constant, e.g., "jx.String"
Nullable bool // true if field is nullable (accepts both base type and jx.Null)

// ArrayElementType is the jx.Type of array elements for array element discrimination.
// Only set when FieldType is "jx.Array" and element type can distinguish variants.
// e.g., "jx.String" for array[string], "jx.Number" for array[integer], "jx.Object" for array[object]
ArrayElementType string

// ArrayElementTypeID is the full type ID for array elements (e.g., "string", "integer", "object").
// Used for more detailed discrimination like distinguishing integer vs number.
ArrayElementTypeID string
}

// SumSpec for KindSum.
Expand Down
10 changes: 9 additions & 1 deletion gen/schema_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"cmp"
"fmt"
"path"
"sort"
"strings"

"github.com/go-faster/errors"
Expand Down Expand Up @@ -865,7 +866,14 @@ func inferSchemaFromObject(obj map[string]any) *jsonschema.Schema {
schema := &jsonschema.Schema{
Type: jsonschema.Object,
}
for fieldName, fieldValue := range obj {
// Sort keys for deterministic output
keys := make([]string, 0, len(obj))
for k := range obj {
keys = append(keys, k)
}
sort.Strings(keys)
for _, fieldName := range keys {
fieldValue := obj[fieldName]
prop := jsonschema.Property{
Name: fieldName,
Schema: inferSchemaFromValue(fieldValue),
Expand Down
Loading
Loading