labstack / echo

High performance, minimalist Go web framework
https://echo.labstack.com
MIT License
29.96k stars 2.23k forks source link

Breaking Change on v4.12.0 When Binding to map[string]interface{} -> the field become list #2652

Closed slzhffktm closed 2 months ago

slzhffktm commented 5 months ago

Issue Description

Breaking change on v4.12.0:

The c.Bind is now mapping query params & request body to list of string.

Example:

POST /test?query=param

request body

{"field1": "somevalue"}

The code

request := make(map[string]interface{})
if err := c.Bind(&request); err != nil {
    return err
}

fmt.Printf("%#v", request)

Previous behaviour (pre v4.12.0):

Result: map[string]interface {}{"query":"param", "field1":"somevalue"}

Current behaviour (v4.12.0):

Result: map[string]interface {}{"query":[]string{"param"}, "field1":[]string{"somevalue"}}

Checklist

Expected behaviour

The behaviour from the code above should keep on returning non-breaking change and still return this on the newest version:

Result: map[string]interface {}{"query":"param", "field1":"somevalue"}

Actual behaviour

In v4.12.0, the result from the code above returned different result:

Result: map[string]interface {}{"query":[]string{"param"}, "field1":[]string{"somevalue"}}

Steps to reproduce

Working code to debug

package main

func main() {
}

Version/commit

v4.12.0

thesaltree commented 4 months ago

@slzhffktm I've submitted a pull request (https://github.com/labstack/echo/pull/2656) that addresses this issue by maintaining backwards compatibility for map[string]interface{} binding while supporting the new functionality.

The PR modifies the bindData function to:

However, if the maintainers decide not to merge this change, here's an alternative solution you can use in your code to achieve the same result:

func ConvertToSingleValues(input map[string]interface{}) map[string]interface{} {
    for key, value := range input {
        if strSlice, ok := value.([]string); ok {
            if len(strSlice) == 1 {
                input[key] = strSlice[0]
            }
        }
    }
    return input
}

func ExampleFunction(c echo.Context) error {
    request := make(map[string]interface{})
    if err := c.Bind(&request); err != nil {
        return err
    }
    request := ConvertToSingleValues(request)
    return c.JSON(http.StatusOK, request)
}

You can use this helper function with new version without breaking code regardless of whether the PR is merged. It processes the map[string]interface{} returned by c.Bind() and converts any []string values to single strings if they contain only one element.

slzhffktm commented 2 months ago

Hello @thesaltree ! Seems like your PR is merged, thank you for your contribution!!

arigon commented 2 weeks ago

However, if the maintainers decide not to merge this change, here's an alternative solution you can use in your code to achieve the same result:


func ConvertToSingleValues(input map[string]interface{}) map[string]interface{} {
    for key, value := range input {
        if strSlice, ok := value.([]string); ok {
            if len(strSlice) == 1 {
                input[key] = strSlice[0]
            }
        }
    }
    return input
}

Your return header is not necessary because map types are reference types:

func ConvertToSingleValues(input map[string]interface{}) {
    for key, value := range input {
        if strSlice, ok := value.([]string); ok {
            if len(strSlice) == 1 {
                input[key] = strSlice[0]
            }
        }
    }
}

I stumbled over this issue because I have the same problem after updating to 4.12. I'm wondering when this fix will be released, so until now I'm relying on this helper function.