package magic import ( "encoding/json" "net/http" "reflect" "strings" "github.com/go-playground/validator/v10" ) var ( jsonValidator = NewValidator("json") ) type JsonDecodeError struct { inner error } func (err JsonDecodeError) Error() string { return "failed to decode json: " + err.inner.Error() } func (err JsonDecodeError) WriteResponse(rw http.ResponseWriter) { rw.WriteHeader(http.StatusBadRequest) } type ValidateError []string func (err ValidateError) Error() string { return "invalid or missing fields: [" + strings.Join(err, ", ") + "]" } func (v ValidateError) WriteResponse(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusBadRequest) w.Write([]byte(v.Error())) } type Json[T any] struct { Data T `json:"data"` } // TakeRequestBody is a marker method to indicate that this extractor takes the request body. func (j Json[T]) TakeRequestBody() {} func NewJson[T any](data T) Json[T] { return Json[T]{ Data: data, } } func (data *Json[T]) FromRequest(request *http.Request) error { if request.Body == nil { panic("body is already taken") } bodyReader := request.Body defer bodyReader.Close() request.Body = nil if err := json.NewDecoder(bodyReader).Decode(&data.Data); err != nil { return JsonDecodeError{inner: err} } value := reflect.ValueOf(data.Data) if value.Kind() == reflect.Struct || (value.Kind() == reflect.Ptr && value.Elem().Kind() == reflect.Struct) { if err := jsonValidator.StructCtx(request.Context(), data.Data); err == nil { return nil } else if validateErr, isValidateErr := err.(validator.ValidationErrors); isValidateErr { fields := make([]string, len(validateErr)) for idx, err := range validateErr { fields[idx] = err.Field() } return ValidateError(fields) } else { return err } } return nil } func (data *Json[T]) WriteResponse(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") if data == nil { return } json.NewEncoder(w).Encode(data) }