package validator import ( "net/http" "reflect" "strings" "github.com/go-playground/validator/v10" "github.com/labstack/echo/v4" ) // CustomValidator wraps go-playground/validator for Echo type CustomValidator struct { validator *validator.Validate } // NewCustomValidator creates a new validator instance func NewCustomValidator() *CustomValidator { v := validator.New() // Use JSON tag names for field names in errors v.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) return &CustomValidator{validator: v} } // Validate implements echo.Validator interface func (cv *CustomValidator) Validate(i interface{}) error { if err := cv.validator.Struct(i); err != nil { return err } return nil } // ValidationErrorResponse is the structured error response format type ValidationErrorResponse struct { Error string `json:"error"` Fields map[string]FieldError `json:"fields,omitempty"` } // FieldError represents a single field validation error type FieldError struct { Message string `json:"message"` Tag string `json:"tag"` } // FormatValidationErrors converts validator errors to structured response func FormatValidationErrors(err error) *ValidationErrorResponse { if validationErrors, ok := err.(validator.ValidationErrors); ok { fields := make(map[string]FieldError) for _, fe := range validationErrors { fieldName := fe.Field() fields[fieldName] = FieldError{ Message: formatMessage(fe), Tag: fe.Tag(), } } return &ValidationErrorResponse{ Error: "Validation failed", Fields: fields, } } return &ValidationErrorResponse{Error: err.Error()} } // HTTPError returns an echo.HTTPError with validation details func HTTPError(c echo.Context, err error) error { return c.JSON(http.StatusBadRequest, FormatValidationErrors(err)) } func formatMessage(fe validator.FieldError) string { switch fe.Tag() { case "required": return "This field is required" case "required_without": return "This field is required when " + fe.Param() + " is not provided" case "required_with": return "This field is required when " + fe.Param() + " is provided" case "email": return "Must be a valid email address" case "min": return "Must be at least " + fe.Param() + " characters" case "max": return "Must be at most " + fe.Param() + " characters" case "len": return "Must be exactly " + fe.Param() + " characters" case "oneof": return "Must be one of: " + fe.Param() case "url": return "Must be a valid URL" case "uuid": return "Must be a valid UUID" default: return "Invalid value" } }