ory/kratos · error

failed to unmarshal notify_previous_addresses configuration…

Error message

failed to unmarshal notify_previous_addresses configuration for %s: %w

What it means

getHooks decodes a notify_previous_addresses hook's optional JSON config into hook.NotifyPreviousAddressesConfig. If the raw bytes are non-empty but invalid JSON or of the wrong shape, construction of the hook chain is aborted with this wrapped error naming the credentials type. Unlike the web_hook case, an empty config is tolerated.

Solutions

  1. Fix the JSON so it parses and matches hook.NotifyPreviousAddressesConfig field types
  2. Leave the config field empty/null if no options are needed — empty config is allowed
  3. Validate with jq -e against the expected schema before applying config
  4. Inspect raw_config in the error log to identify the exact malformed input

Example fix

// before
{"job": "notify_previous_addresses", "config": ["a", "b"]}
// after
{"job": "notify_previous_addresses", "config": {}}
Defensive patterns

Strategy: validation

Validate before calling

// Go: validate notify_previous_addresses config before use
func validNotifyPrevConfig(raw json.RawMessage) error {
	if len(raw) == 0 {
		return nil // empty config is allowed
	}
	var cfg hook.NotifyPreviousAddressesConfig
	return json.Unmarshal(raw, &cfg)
}

Type guard

func isNotifyPrevConfigOK(raw json.RawMessage) bool {
	if len(raw) == 0 {
		return true
	}
	var cfg hook.NotifyPreviousAddressesConfig
	return json.Unmarshal(raw, &cfg) == nil
}

Try / catch

hooks, err := reg.PostRecoveryHooks(ctx, credentialsType)
if err != nil {
	log.WithError(err).Error("check notify_previous_addresses hook config JSON")
	return err
}

Prevention

When it happens

Trigger: A hook with key "notify_previous_addresses" has a non-empty Config field that fails json.Unmarshal into &hook.NotifyPreviousAddressesConfig{} (malformed JSON or incompatible field types).

Common situations: Hand-edited JSON in Kratos config with typos; pasting YAML into the JSON config block; wrapping the config in an extra array; using the wrong casing for config keys after copy-paste between environments.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of ory/kratos@b86338da04 (2026-09-07). Data as JSON: /api/errors/acf578f6d930b812. Report an issue: GitHub.

Appendix: source

Thrown at driver/registry_default_hooks.go:114

			}
		case hook.KeyVerificationUI:
			if h, ok := any(m.HookShowVerificationUI()).(T); ok {
				hooks = append(hooks, h)
			}
		case hook.KeyVerifier:
			if h, ok := any(m.HookVerifier()).(T); ok {
				hooks = append(hooks, h)
			}
		case hook.KeyVerifyNewAddress:
			if h, ok := any(m.HookVerifyNewAddress()).(T); ok {
				hooks = append(hooks, h)
			}
		case hook.KeyNotifyPreviousAddresses:
			cfg := &hook.NotifyPreviousAddressesConfig{}
			if len(hookConfig.Config) > 0 {
				if err := json.Unmarshal(hookConfig.Config, cfg); err != nil {
					m.l.WithError(err).WithField("raw_config", string(hookConfig.Config)).Error("failed to unmarshal hook configuration, ignoring hook")
					return nil, errors.WithStack(fmt.Errorf("failed to unmarshal notify_previous_addresses configuration for %s: %w", credentialsType, err))
				}
			}
			if h, ok := any(m.HookNotifyPreviousAddresses(cfg)).(T); ok {
				hooks = append(hooks, h)
			}
		default:
			for name, newHook := range m.injectedSelfserviceHooks {
				if name == hookConfig.Name {
					if h, ok := newHook(hookConfig, m).(T); ok {
						hooks = append(hooks, h)
					}
					continue allHooksLoop
				}
			}
			m.l.
				WithField("for", credentialsType).
				WithField("hook", hookConfig.Name).
				Warn("A configuration for a non-existing hook was found and will be ignored.")

View on GitHub (pinned to b86338da04)