Как вернуть пользовательскую ошибку, если проверка с помощью Gin завершилась неудачно

Например, у меня есть такой struct

type Address struct {
    City string `json:"city" binding:"required"`
    AddressLine string `json:"address_line" binding:"required"`
}

и у меня есть следующая функция для обработки запросов от пользователей

func AddressCreate(c *gin.Context) {
    var address Address
    if err := c.BindJSON(&address); err == nil {
        // if everything is good save to database
        // and return success message
        db.Create(&address)
        c.JSON(http.StatusOK, gin.H {"status":"success"})
    } else {
        c.JSON(http.StatusBadRequest, err)
    }
}

Ожидаемое поведение - возврат JSON, отформатированный таким образом

[
     {
         "city":"required"
     }
     {
         "address_line":"required"
     }
]

Но я получаю сообщение об ошибке в таком формате

"Address.City": {
    "FieldNamespace": "Address.City",
    "NameNamespace": "City",
    "Field": "City",
    "Name": "City",
    "Tag": "required",
    "ActualTag": "required",
    "Kind": 24,
    "Type": {},
    "Param": "",
    "Value": ""
},
"Address.AddressLine": {
    "FieldNamespace": "AddressLine",
    "NameNamespace": "AddressLine",
    "Field": "AddressLine",
    "Name": "AddressLine",
    "Tag": "required",
    "ActualTag": "required",
    "Kind": 24,
    "Type": {},
    "Param": "",
    "Value": ""
}

Что я пробовал:
Я создал функцию, которая преобразует error в ValidationErrors и перебирает все FieldError в ней

func ListOfErrors(e error) []map[string]string {
    ve := e.(validator.ValidationErrors)
    InvalidFields := make([]map[string]string, 0)

    for _, e := range ve {
        errors := map[string]string{}
        // field := reflect.TypeOf(e.NameNamespace)
        errors[e.Name] = e.Tag
        InvalidFields = append(InvalidFields, errors)
    }

    return InvalidFields
}

Результат выглядит намного лучше

[
    {
        "City":"required"
    },
    {
        "AddressLine":"required"
    }
]

Но не могу решить проблему с названием полей. Я не могу поменять Name на name, который я отметил в теге структур json:"city". Итак, мой вопрос: выбрал ли я правильный способ решения проблемы, если ответ положительный, как получить тег структуры JSON для поля?


person latsha    schedule 25.05.2018    source источник


Ответы (2)


Вы можете использовать ToSnake, чтобы ввести имена змейкой:

import (
    "unicode"
)

// ToSnake convert the given string to snake case following the Golang format:
// acronyms are converted to lower-case and preceded by an underscore.
func ToSnake(in string) string {
    runes := []rune(in)
    length := len(runes)

    var out []rune
    for i := 0; i < length; i++ {
        if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
            out = append(out, '_')
        }
        out = append(out, unicode.ToLower(runes[i]))
    }

    return string(out)
}



func ListOfErrors(e error) []map[string]string {
    ve := e.(validator.ValidationErrors)
    invalidFields := make([]map[string]string, 0)

    for _, e := range ve {
        errors := map[string]string{}
        errors[ToSnake(e.Name)] = e.Tag
        invalidFields = append(InvalidFields, errors)
    }

    return invalidFields
}
person Alireza Bashiri    schedule 25.05.2018
comment
Спасибо за ответ, но ToLower не будет работать во втором случае, если имя поля AddressLine, потому что я ожидаю address_line - person latsha; 25.05.2018
comment
обновил ответ. вы также можете использовать сторонние библиотеки. github.com/iancoleman/strcase - person Alireza Bashiri; 25.05.2018

Если вы хотите, чтобы он был таким же, как определено в вашем теге json, вам следует использовать отражение, чтобы извлечь этот тег из вашего типа данных.

У меня нет ваших библиотек, поэтому я не могу его скомпилировать и проверить. Но я считаю, что то, что вам нужно, должно соответствовать этим принципам:

func ListOfErrors(address *Address, e error) []map[string]string {
    ve := e.(validator.ValidationErrors)
    InvalidFields := make([]map[string]string, 0)

    for _, e := range ve {
        errors := map[string]string{}
        // field := reflect.TypeOf(e.NameNamespace)
        field, _ := reflect.TypeOf(address).Elem().FieldByName(e.Name)
        jsonTag := string(field.Tag.Get("json"))
        errors[jsonTag] = e.Tag
        InvalidFields = append(InvalidFields, errors)
    }

    return InvalidFields
}

Обратите внимание, что это немного надумано, поскольку тип параметра address по сути известен. Таким образом, не обязательно в качестве параметра функции. Но вы можете изменить address *Address на address interface{} и использовать его и для других типов.

Отказ от ответственности: я пропустил проверку ошибок для краткости, но вы обязательно должны проверить ошибки в своем коде (например, отсутствие такой ошибки поля или тега json в этом поле).

person Seva    schedule 25.05.2018
comment
Спасибо за ответ. Можно ли не передавать Address в качестве параметра? Я хочу создать решение для всех типов не только для Address. - person latsha; 25.05.2018
comment
Вот о чем была записка. Если вы измените тип параметра address на interface{}, вы можете использовать его с любым типом, а не только с адресом, и он будет работать. Но вам все равно нужно передать объект, который вы проверяли. Go - это статически типизированный язык, поэтому вам нужен объект, чтобы получить его тип. Но я не знаю ни одного API, который давал бы вам ввод по имени типа. - person Seva; 25.05.2018