ory/hydra · error
cookiex: payload must be a flat JSON object with string valu
Error message
cookiex: payload must be a flat JSON object with string values while legacy encode is enabled
What it means
When legacy encode mode is enabled, sealLegacy serializes the cookie payload and unmarshals it into map[string]string to feed the legacy securecookie codec. If the JSON round-trip fails (values are not all strings) or the map is nil, this error is returned because the legacy format can only encode flat string-valued objects.
Source
Thrown at oryx/cookiex/legacy_securecookie.go:125
// payload are coerced to empty strings by the bridge; do not use pointer-typed
// fields while legacy encode is enabled.
func WithLegacyEncode() Option {
return func(c *config) { c.legacyEncode = true }
}
// sealLegacy bridges T through its JSON representation into the flat
// string-to-string map that the securecookie stores used.
func (c *Codec[T]) sealLegacy(name string, value T) (string, error) {
buf, err := json.Marshal(value)
if err != nil {
return "", errors.Wrap(err, "cookiex: cannot marshal cookie value")
}
var flat map[string]string
if err := json.Unmarshal(buf, &flat); err != nil {
return "", errors.Wrap(err, "cookiex: payload must be a flat JSON object with string values while legacy encode is enabled")
}
if flat == nil {
return "", errors.New("cookiex: payload must be a flat JSON object with string values while legacy encode is enabled")
}
values := make(map[any]any, len(flat))
for k, v := range flat {
values[k] = v
}
encoded, err := securecookie.EncodeMulti(name, values, c.legacy.codecs[0])
if err != nil {
return "", errors.Wrap(err, "cookiex: cannot encode legacy cookie")
}
return encoded, nil
}
View on GitHub (pinned to 4174065ffb)
Solutions
- Convert all payload values to strings before Set: use strconv.Itoa/FormatBool or fmt.Sprint for each value.
- Store nested/structured data as a single JSON-encoded string value.
- If non-string values are required, disable WithLegacyEncode and use the modern codec path.
- Use errors.As/Is on the wrapped json.Unmarshal error to distinguish a malformed payload from the nil-map case.
Example fix
// before
vals := map[string]any{"count": 42}
c.Set(w, "sess", vals)
// after
vals := map[string]any{"count": strconv.Itoa(42)}
c.Set(w, "sess", vals) Defensive patterns
Strategy: validation
Validate before calling
func stringifyValues(in map[string]any) (map[string]any, error) {
out := make(map[string]any, len(in))
for k, v := range in {
s, ok := v.(string)
if !ok { return nil, fmt.Errorf("cookie value %q is not a string", k) }
out[k] = s
}
return out, nil
}
// apply before c.Set while legacy encode is enabled Try / catch
if _, err := c.Set(w, name, vals); err != nil {
var se *json.SyntaxError
if errors.As(err, &se) || strings.Contains(err.Error(), "flat JSON object") {
return fmt.Errorf("cookie payload has non-string values: %w", err)
}
return err
} Prevention
- Convert numeric/bool payload fields to strings with strconv before Set
- Keep nested structures JSON-encoded inside a single string value
- Remember the legacy path only supports map[string]string; test payloads during migration
When it happens
Trigger: Calling Set (which delegates to sealLegacy) on a cookiex codec created with WithLegacyEncode(true), passing a values map whose JSON contains non-string values (numbers, booleans, nested objects, arrays) or when the payload unmarshals to a nil map.
Common situations: Migrating an app that previously stored ints/bools in session cookies (e.g. "count": 42) onto cookiex with legacy encode on; storing nested structures that the old encoder silently accepted via gob but this path rejects.
Related errors
- cookiex: legacy encode requires legacy key pairs
- unable to scan type %T as JSON into %T
- cannot scan %#v into StringSliceJSONFormat
- expected JSON value to be an array but got type: %s
- cannot scan %T into JSONRawMessage
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/a1025213716d270f.
Report an issue: GitHub.