11package fiberoapi
22
33import (
4+ "encoding/json"
45 "fmt"
56 "reflect"
67 "strconv"
@@ -50,8 +51,9 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op
5051 // It's OK, the POST has no body - ignore the error
5152 } else {
5253 // Transform JSON unmarshal type errors into readable validation errors
53- // Check if error message contains unmarshal type error pattern
5454 errMsg := err .Error ()
55+
56+ // Check if error message contains unmarshal type error pattern
5557 if strings .Contains (errMsg , "json: cannot unmarshal" ) && strings .Contains (errMsg , "into Go struct field" ) {
5658 // Parse the error message to extract field name and type info
5759 // Format: "json: cannot unmarshal <type> into Go struct field <StructName>.<Field> of type <GoType>"
@@ -75,8 +77,20 @@ func parseInput[TInput any](app *OApiApp, c *fiber.Ctx, path string, options *Op
7577 fieldName , expectedType , typePart )
7678 }
7779 }
80+ } else if strings .Contains (errMsg , "json: slice" ) || strings .Contains (errMsg , "json: map" ) {
81+ // Handle "json: slice unexpected end of JSON input" and similar errors
82+ // This happens when sending wrong type for slice/map fields
83+ // Try to identify which field caused the error by parsing the request body
84+ fieldName , expectedType , actualType := detectTypeMismatchFromBody (c .Body (), input )
85+ if fieldName != "" {
86+ return input , fmt .Errorf ("invalid type for field '%s': expected %s but got %s" ,
87+ fieldName , expectedType , actualType )
88+ }
89+ // Fallback to generic message if we can't identify the field
90+ return input , fmt .Errorf ("invalid JSON: expected array or object but got incompatible type" )
7891 }
7992
93+ // Return original error if no pattern matched
8094 return input , err
8195 }
8296 }
@@ -441,6 +455,116 @@ func getSchemaForType(t reflect.Type) map[string]interface{} {
441455 return schema
442456}
443457
458+ // detectTypeMismatchFromBody attempts to identify which field caused a JSON type mismatch
459+ // by parsing the request body and comparing against the expected struct type
460+ func detectTypeMismatchFromBody (body []byte , input interface {}) (fieldName , expectedType , actualType string ) {
461+ // Parse the JSON body into a map to see what was actually sent
462+ var bodyMap map [string ]interface {}
463+ if err := json .Unmarshal (body , & bodyMap ); err != nil {
464+ return "" , "" , ""
465+ }
466+
467+ // Get the struct type using reflection
468+ inputValue := reflect .ValueOf (input )
469+ if inputValue .Kind () == reflect .Ptr {
470+ inputValue = inputValue .Elem ()
471+ }
472+ inputType := inputValue .Type ()
473+
474+ if inputType .Kind () != reflect .Struct {
475+ return "" , "" , ""
476+ }
477+
478+ // Iterate through struct fields to find the mismatch
479+ for i := 0 ; i < inputType .NumField (); i ++ {
480+ field := inputType .Field (i )
481+
482+ // Get the JSON tag name (default to field name if no tag)
483+ jsonTag := field .Tag .Get ("json" )
484+ if jsonTag == "" {
485+ jsonTag = field .Name
486+ } else {
487+ // Remove omitempty and other options from the tag
488+ jsonTag = strings .Split (jsonTag , "," )[0 ]
489+ }
490+
491+ // Check if this field is in the body map
492+ if actualValue , exists := bodyMap [jsonTag ]; exists {
493+ expectedFieldType := dereferenceType (field .Type )
494+ actualValueType := getJSONValueType (actualValue )
495+
496+ // Check for type mismatch
497+ mismatch := false
498+ expectedTypeName := ""
499+
500+ switch expectedFieldType .Kind () {
501+ case reflect .String :
502+ expectedTypeName = "string"
503+ if actualValueType != "string" {
504+ mismatch = true
505+ }
506+ case reflect .Int , reflect .Int8 , reflect .Int16 , reflect .Int32 , reflect .Int64 ,
507+ reflect .Uint , reflect .Uint8 , reflect .Uint16 , reflect .Uint32 , reflect .Uint64 :
508+ expectedTypeName = "integer"
509+ if actualValueType != "number" {
510+ mismatch = true
511+ }
512+ case reflect .Float32 , reflect .Float64 :
513+ expectedTypeName = "number"
514+ if actualValueType != "number" {
515+ mismatch = true
516+ }
517+ case reflect .Bool :
518+ expectedTypeName = "boolean"
519+ if actualValueType != "boolean" {
520+ mismatch = true
521+ }
522+ case reflect .Slice , reflect .Array :
523+ expectedTypeName = fmt .Sprintf ("[]%s" , dereferenceType (expectedFieldType .Elem ()).Kind ())
524+ if actualValueType != "array" {
525+ mismatch = true
526+ }
527+ case reflect .Map :
528+ expectedTypeName = "map"
529+ if actualValueType != "object" {
530+ mismatch = true
531+ }
532+ case reflect .Struct :
533+ expectedTypeName = "object"
534+ if actualValueType != "object" {
535+ mismatch = true
536+ }
537+ }
538+
539+ if mismatch {
540+ return field .Name , expectedTypeName , actualValueType
541+ }
542+ }
543+ }
544+
545+ return "" , "" , ""
546+ }
547+
548+ // getJSONValueType returns the JSON type name for a value parsed from JSON
549+ func getJSONValueType (value interface {}) string {
550+ switch value .(type ) {
551+ case string :
552+ return "string"
553+ case float64 , int , int64 :
554+ return "number"
555+ case bool :
556+ return "boolean"
557+ case []interface {}:
558+ return "array"
559+ case map [string ]interface {}:
560+ return "object"
561+ case nil :
562+ return "null"
563+ default :
564+ return "unknown"
565+ }
566+ }
567+
444568// mergeParameters merges auto-generated parameters with manually defined ones
445569// Manual parameters take precedence over auto-generated ones with the same name
446570func mergeParameters (autoParams []map [string ]interface {}, manualParams []map [string ]interface {}) []map [string ]interface {} {
0 commit comments