Skip to content

Commit e5d4465

Browse files
authored
Enhance JSON type mismatch error handling (#23)
* Enhance JSON type mismatch error handling tests with additional cases for new fields * Improve JSON type mismatch error handling with detailed field identification
1 parent 590f30d commit e5d4465

2 files changed

Lines changed: 427 additions & 3 deletions

File tree

common.go

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package fiberoapi
22

33
import (
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
446570
func mergeParameters(autoParams []map[string]interface{}, manualParams []map[string]interface{}) []map[string]interface{} {

0 commit comments

Comments
 (0)