@@ -305,11 +305,19 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
305305 jsonschema .Number ,
306306 jsonschema .Boolean ,
307307 jsonschema .Null :
308- default :
309- return nil , errors .Wrap (
310- & ErrNotImplemented {Name : "non-primitive enum" },
311- name ,
312- )
308+ // Primitive enums are handled below
309+ case jsonschema .Object :
310+ // Non-primitive object enums generate sum types with struct variants.
311+ // Each enum value becomes a concrete struct type.
312+ t , err := g .nonPrimitiveObjectEnum (name , schema )
313+ if err != nil {
314+ return nil , errors .Wrap (err , "non-primitive object enum" )
315+ }
316+ return t , nil
317+ case jsonschema .Array , jsonschema .Empty :
318+ // Array enums and empty type enums are treated as "any" type.
319+ // The enum constraint is documented in OpenAPI but not enforced at runtime.
320+ return g .regtype (name , ir .Any (schema )), nil
313321 }
314322 }
315323
@@ -709,3 +717,209 @@ func (g *schemaGen) checkDefaultType(s *jsonschema.Schema, val any) error {
709717
710718 return nil
711719}
720+
721+ // nonPrimitiveObjectEnum generates a sum type for object enums.
722+ // Each enum value becomes a concrete struct variant.
723+ func (g * schemaGen ) nonPrimitiveObjectEnum (name string , schema * jsonschema.Schema ) (* ir.Type , error ) {
724+ if len (schema .Enum ) == 0 {
725+ return nil , errors .New ("enum has no values" )
726+ }
727+
728+ // Convert enum values to map[string]any
729+ enumObjects := make ([]map [string ]any , 0 , len (schema .Enum ))
730+ for i , v := range schema .Enum {
731+ obj , ok := v .(map [string ]any )
732+ if ! ok {
733+ return nil , errors .Errorf ("enum[%d]: expected object, got %T" , i , v )
734+ }
735+ enumObjects = append (enumObjects , obj )
736+ }
737+
738+ // Find a discriminating field - a string field with unique values across all variants
739+ discriminatorField , variantNames := findEnumDiscriminator (enumObjects )
740+ if discriminatorField == "" {
741+ // No discriminator found, fall back to index-based naming
742+ for i := range enumObjects {
743+ variantNames = append (variantNames , fmt .Sprintf ("Variant%d" , i ))
744+ }
745+ }
746+
747+ // Create the sum type
748+ sum := g .regtype (name , & ir.Type {
749+ Name : name ,
750+ Kind : ir .KindSum ,
751+ Schema : schema ,
752+ })
753+
754+ // Generate struct types for each enum value
755+ variants := make ([]* ir.Type , 0 , len (enumObjects ))
756+ for i , obj := range enumObjects {
757+ variantName := name + variantNames [i ]
758+ variantSchema := inferSchemaFromObject (obj )
759+
760+ // Generate the variant struct type
761+ variantType := & ir.Type {
762+ Kind : ir .KindStruct ,
763+ Name : variantName ,
764+ Schema : variantSchema ,
765+ }
766+
767+ // Add fields from the object
768+ for fieldName , fieldValue := range obj {
769+ fieldSchema := inferSchemaFromValue (fieldValue )
770+ fieldType := g .inferTypeFromValue (fieldValue , fieldSchema )
771+
772+ field := & ir.Field {
773+ Name : naming .Capitalize (fieldName ),
774+ Type : fieldType ,
775+ Tag : ir.Tag {
776+ JSON : fieldName ,
777+ },
778+ Spec : & jsonschema.Property {
779+ Name : fieldName ,
780+ Schema : fieldSchema ,
781+ Required : true ,
782+ },
783+ }
784+ variantType .Fields = append (variantType .Fields , field )
785+ }
786+
787+ // Register and add to variants
788+ g .regtype (variantName , variantType )
789+ variants = append (variants , variantType )
790+ }
791+
792+ sum .SumOf = variants
793+
794+ // Set up discrimination
795+ if discriminatorField != "" {
796+ // Value-based discrimination on the discriminator field
797+ valueToVariant := make (map [string ]string )
798+ for i , obj := range enumObjects {
799+ if val , ok := obj [discriminatorField ].(string ); ok {
800+ valueToVariant [val ] = variants [i ].Name + name
801+ }
802+ }
803+ sum .SumSpec .ValueDiscriminators = map [string ]ir.ValueDiscriminator {
804+ discriminatorField : {
805+ FieldName : discriminatorField ,
806+ ValueToVariant : valueToVariant ,
807+ },
808+ }
809+ } else {
810+ // No discriminator field found, use type-based discrimination as fallback
811+ sum .SumSpec .TypeDiscriminator = true
812+ }
813+
814+ return sum , nil
815+ }
816+
817+ // findEnumDiscriminator finds a string field that has unique values across all enum objects.
818+ func findEnumDiscriminator (objects []map [string ]any ) (string , []string ) {
819+ if len (objects ) == 0 {
820+ return "" , nil
821+ }
822+
823+ // Find all string fields present in all objects
824+ stringFields := make (map [string ][]string )
825+ for _ , obj := range objects {
826+ for k , v := range obj {
827+ if s , ok := v .(string ); ok {
828+ stringFields [k ] = append (stringFields [k ], s )
829+ }
830+ }
831+ }
832+
833+ // Find a field with unique values across all objects
834+ for field , values := range stringFields {
835+ if len (values ) != len (objects ) {
836+ continue // Field not present in all objects
837+ }
838+
839+ // Check if all values are unique
840+ seen := make (map [string ]bool )
841+ allUnique := true
842+ for _ , v := range values {
843+ if seen [v ] {
844+ allUnique = false
845+ break
846+ }
847+ seen [v ] = true
848+ }
849+
850+ if allUnique {
851+ // Use these values as variant names (capitalized)
852+ variantNames := make ([]string , len (values ))
853+ for i , v := range values {
854+ variantNames [i ] = naming .Capitalize (v )
855+ }
856+ return field , variantNames
857+ }
858+ }
859+
860+ return "" , nil
861+ }
862+
863+ // inferSchemaFromObject creates a jsonschema.Schema from an object literal.
864+ func inferSchemaFromObject (obj map [string ]any ) * jsonschema.Schema {
865+ schema := & jsonschema.Schema {
866+ Type : jsonschema .Object ,
867+ }
868+ for fieldName , fieldValue := range obj {
869+ prop := jsonschema.Property {
870+ Name : fieldName ,
871+ Schema : inferSchemaFromValue (fieldValue ),
872+ Required : true ,
873+ }
874+ schema .Properties = append (schema .Properties , prop )
875+ }
876+ return schema
877+ }
878+
879+ // inferSchemaFromValue creates a jsonschema.Schema from a JSON value.
880+ func inferSchemaFromValue (v any ) * jsonschema.Schema {
881+ switch val := v .(type ) {
882+ case string :
883+ return & jsonschema.Schema {Type : jsonschema .String }
884+ case int64 :
885+ return & jsonschema.Schema {Type : jsonschema .Integer }
886+ case float64 :
887+ return & jsonschema.Schema {Type : jsonschema .Number }
888+ case bool :
889+ return & jsonschema.Schema {Type : jsonschema .Boolean }
890+ case nil :
891+ return & jsonschema.Schema {Type : jsonschema .Null }
892+ case []any :
893+ schema := & jsonschema.Schema {Type : jsonschema .Array }
894+ if len (val ) > 0 {
895+ schema .Item = inferSchemaFromValue (val [0 ])
896+ }
897+ return schema
898+ case map [string ]any :
899+ return inferSchemaFromObject (val )
900+ default :
901+ return & jsonschema.Schema {}
902+ }
903+ }
904+
905+ // inferTypeFromValue creates an ir.Type from a JSON value.
906+ func (g * schemaGen ) inferTypeFromValue (v any , schema * jsonschema.Schema ) * ir.Type {
907+ switch v .(type ) {
908+ case string :
909+ return ir .Primitive (ir .String , schema )
910+ case int64 :
911+ return ir .Primitive (ir .Int64 , schema )
912+ case float64 :
913+ return ir .Primitive (ir .Float64 , schema )
914+ case bool :
915+ return ir .Primitive (ir .Bool , schema )
916+ case nil :
917+ return ir .Primitive (ir .Null , schema )
918+ case []any :
919+ return ir .Array (g .inferTypeFromValue (nil , nil ), ir .NilInvalid , schema )
920+ case map [string ]any :
921+ return ir .Any (schema )
922+ default :
923+ return ir .Any (schema )
924+ }
925+ }
0 commit comments