go-redis/redis · error

FT.CREATE: ON HASH and ON JSON are mutually exclusive

Error message

FT.CREATE: ON HASH and ON JSON are mutually exclusive

What it means

Returned by FTCreate when FTCreateOptions has both OnHash and OnJSON set to true. An FT.CREATE index targets exactly one document type (HASH or JSON); specifying both is contradictory, so the builder attaches the error to the returned StatusCmd instead of sending an invalid command.

Source

Thrown at search_commands.go:1395

// FTCreate - Creates a new index with the given options and schema.
// The 'index' parameter specifies the name of the index to create.
// The 'options' parameter specifies various options for the index, such as:
// whether to index hashes or JSONs, prefixes, filters, default language, score, score field, payload field, etc.
// The 'schema' parameter specifies the schema for the index, which includes the field name, field type, etc.
// For more information, please refer to the Redis documentation:
// [FT.CREATE]: (https://redis.io/commands/ft.create/)
func (c cmdable) FTCreate(ctx context.Context, index string, options *FTCreateOptions, schema ...*FieldSchema) *StatusCmd {
	args := []interface{}{"FT.CREATE", index}
	if options != nil {
		if options.OnHash && !options.OnJSON {
			args = append(args, "ON", "HASH")
		}
		if options.OnJSON && !options.OnHash {
			args = append(args, "ON", "JSON")
		}
		if options.OnHash && options.OnJSON {
			cmd := NewStatusCmd(ctx, args...)
			cmd.SetErr(fmt.Errorf("FT.CREATE: ON HASH and ON JSON are mutually exclusive"))
			return cmd
		}
		if options.Prefix != nil {
			args = append(args, "PREFIX", len(options.Prefix))
			args = append(args, options.Prefix...)
		}
		if options.Filter != "" {
			args = append(args, "FILTER", options.Filter)
		}
		if options.DefaultLanguage != "" {
			args = append(args, "LANGUAGE", options.DefaultLanguage)
		}
		if options.LanguageField != "" {
			args = append(args, "LANGUAGE_FIELD", options.LanguageField)
		}
		if options.Score > 0 {
			args = append(args, "SCORE", options.Score)
		}

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Set exactly one of OnHash or OnJSON (or leave both false to let the server apply its default, typically HASH).
  2. Audit the FTCreateOptions literal at the call site to enforce mutual exclusion.
  3. When exposing the choice to users, coerce a single enum into OnHash xor OnJSON before building options.

Example fix

// before
client.FTCreate(ctx, "idx", &FTCreateOptions{OnHash: true, OnJSON: true}, schema...)
// after
client.FTCreate(ctx, "idx", &FTCreateOptions{OnJSON: true}, schema...)
Defensive patterns

Strategy: validation

Validate before calling

func validateFTCreateOn(opts *FTCreateOptions) error {
	if opts.OnHash && opts.OnJSON {
		return fmt.Errorf("choose HASH or JSON, not both")
	}
	return nil
}

Type guard

func onChoiceIsValid(opts *FTCreateOptions) bool {
	return !(opts.OnHash && opts.OnJSON)
}

Try / catch

cmd := client.FTCreate(ctx, "idx", opts, schema...)
if err := cmd.Err(); err != nil {
    if strings.Contains(err.Error(), "ON HASH and ON JSON are mutually exclusive") {
        // clear one of OnHash/OnJSON and retry
    }
    return err
}

Prevention

When it happens

Trigger: Calling FTCreate with &FTCreateOptions{OnHash: true, OnJSON: true}. The error is set on the returned *StatusCmd and surfaces on .Err()/.Result(). Triggered before any FT.CREATE argument beyond the index name is serialized.

Common situations: Defaulting both flags true in a config struct. UI that exposes HASH and JSON as independent toggles and allows both selected. Reusing an options value and flipping one flag without clearing the other. Migrating between HASH and JSON indexes without resetting the prior flag.

Related errors


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/e4ef29845b9acc1a.json. Report an issue: GitHub.