labstack/echo · error

query/param/form tags are not allowed with anonymous struct

Error message

query/param/form tags are not allowed with anonymous struct field

What it means

Returned by bindData when an anonymous (embedded) struct field carries an explicit param/query/form/header tag. The binder descends into anonymous struct fields to bind their inner tagged fields, but cannot resolve which inner field a tag on the outer anonymous field refers to. Echo forbids binding tags directly on embedded struct fields to avoid this ambiguity.

Source

Thrown at bind.go:272

	}

	meta := bindMetaFor(typ)
	for fi := range meta.fields { // iterate over all destination fields
		fm := &meta.fields[fi]
		structField := val.Field(fm.index)
		if fm.anonymous {
			if structField.Kind() == reflect.Pointer {
				structField = structField.Elem()
			}
		}
		if !structField.CanSet() {
			continue
		}
		structFieldKind := structField.Kind()
		inputFieldName := fm.tagName(tag)
		if fm.anonymous && structFieldKind == reflect.Struct && inputFieldName != "" {
			// if anonymous struct with query/param/form tags, report an error
			return errors.New("query/param/form tags are not allowed with anonymous struct field")
		}

		if inputFieldName == "" {
			// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contain fields with tags).
			// structs that implement BindUnmarshaler are bound only when they have explicit tag
			if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
				if err := bindData(structField.Addr().Interface(), data, tag, dataFiles); err != nil {
					return err
				}
			}
			// does not have explicit tag and is not an ordinary struct - so move to next field
			continue
		}

		if hasFiles {
			if ok, err := isFieldMultipartFile(structField.Type()); err != nil {
				return err
			} else if ok {

View on GitHub (pinned to 05489dc173)

Solutions

  1. Remove the binding tag from the anonymous/embedded field; put tags on the inner struct's own fields instead
  2. If you need the field bound by a specific name, make it a non-anonymous (named) field and add the tag there
  3. Implement BindUnmarshaler on the embedded type and bind it via a named field with an explicit tag

Example fix

// before
type Req struct {
    Pagination `query:"page"`
}

// after
type Req struct {
    Pagination
}
Defensive patterns

Strategy: validation

Validate before calling

// Scan struct fields at startup for tags on anonymous fields
func checkNoTagsOnAnonymous(rt reflect.Type) error {
    for i := 0; i < rt.NumField(); i++ {
        f := rt.Field(i)
        if f.Anonymous && hasBindTag(f.Tag) {
            return fmt.Errorf("anonymous field %s must not have bind tags", f.Name)
        }
    }
    return nil
}
func hasBindTag(t reflect.StructTag) bool {
    return t.Get("param") != "" || t.Get("query") != "" || t.Get("form") != "" || t.Get("header") != ""
}

Try / catch

if err := c.Bind(&req); err != nil {
    if strings.Contains(err.Error(), "anonymous struct field") {
        // fix struct definition: remove tag from embedded field
        log.Fatal("remove bind tag from embedded field in ", reflect.TypeOf(req).Name())
    }
    return err
}

Prevention

When it happens

Trigger: Defining a struct with an embedded struct field that has a `query:"..."`, `param:"..."`, `form:"..."`, or `header:"..."` tag, then calling c.Bind / BindQueryParams / BindPathValues / BindHeaders with that struct.

Common situations: Embedding a shared base struct (e.g. Pagination) and accidentally adding a query/form tag to the embedding line. OpenAPI/Swagger code generators that attach tags to promoted embedded fields.

Related errors


AI-assisted analysis of labstack/echo@05489dc173 (2026-08-04). Data as JSON: /data/errors/bb3c3100602f0031.json. Report an issue: GitHub.