itchyny / gojq

Pure Go implementation of jq
MIT License
3.3k stars 119 forks source link

jq: query failed: %!v(PANIC=Error method: invalid type: []map[string]interface {} #227

Closed andig closed 1 year ago

andig commented 1 year ago

I'm trying to use jq directly against Go structures instead of unmarshaled JSON. This test case fails:

func TestQueryAny(t *testing.T) {
        // `{"loadpoints":[{"id":1}]}` 
    var data any = map[string]any{
        "loadpoints": []map[string]any{
            {
                "id": 1,
            },
        },
    }

    // uncomment to fix
    // j, _ := json.Marshal(data)
    // _ = json.Unmarshal(j, &data)

    query, err := gojq.Parse(".loadpoints[0].id")
    assert.NoError(t, err)

    iter := query.Run(data)

    v, ok := iter.Next()
    assert.True(t, ok)

    if err, ok := v.(error); ok {
        assert.NoError(t, err)
    }
}

Running the same query on the unmarshaled json works fine. Please advise if this is unexpected usage of the library?

wader commented 1 year ago

@andig hey, i think you want:

data := map[string]any{
    "loadpoints": []any{
        map[string]any{
            "id": 1,
        },
    },
}

[]map[string] is not a type used by gojq, it wants []any

andig commented 1 year ago

Could you point me to where in the code that happens? Should bot be too difficult, to assign type to any in this case, but its really ugly to do this for the source any.

wader commented 1 year ago

I guess the panic you see if from here https://github.com/itchyny/gojq/blob/bf454af8ae4b652543220a5a67456109224518de/type.go#L27 or https://github.com/itchyny/gojq/blob/bf454af8ae4b652543220a5a67456109224518de/encoder.go#L72 i think @itchyny probably have to answer about assigning/converting but i guess for implementation simplicity and to model the allowed types as close to JSON as possible (array elements can have mixed types)

andig commented 1 year ago

Thank you for the pointers. These two would be simple to fix, maybe like this:

func TypeOf(v any) string {
    switch v.(type) {
    case nil:
        return "null"
    case bool:
        return "boolean"
    case int, float64, *big.Int:
        return "number"
    case string:
        return "string"
    default:
        switch typ := reflect.TypeOf(v); typ.Kind() {
        case reflect.Slice:
            return "array"
        case reflect.Map:
            if typ.Key().Kind() == reflect.String {
                return "object"
            }
            fallthrough
        default:
            panic(fmt.Sprintf("invalid type: %[1]T (%[1]v)", v))
        }
    }
}

However, there is a larger bunch of type checks around like these:

case int, float64, *big.Int:
    i, _ := toInt(x)
    switch v := v.(type) {
    case nil:
        return nil
    case []any:
        return index(v, i)
    case string:
        return indexString(v, i)
    default:
        return &expectedArrayError{v}

Changing every single occurrence would be quite a refactor. It seems it would be easier to apply this transformation on the source than in gojq after all. That said- that is basically what passing it through json does apparently.

itchyny commented 1 year ago

This limitation is mentioned in the README.md.

The type should be []any for an array and map[string]any for a map (just like decoded to an any using the encoding/json package). You can't use []int or map[string]string, for example.

This limitation not only simplifies the implementation, but improves the performance (reflection is slow), and also makes the library extention easy (consider if WithFunction users have to handle any type of slice and map).

This is a design decision of this library. Closing as wontfix.

andig commented 1 year ago

Thank you for the feedback and sorry for not noticing this in the README! Happy with the wontfix :)