ffcodegen is the code generation companion to ffcraft.
It reads authoring YAML or normalized YAML and emits application-facing generated code.
Role split:
ffcompile: produces runtime configuration such asflagdJSON orGO Feature FlagYAMLffcodegen: produces application-facing typed APIs from the same source definition
Today the supported target is go.
Generate Go accessors from authoring YAML:
go run ./cmd/ffcodegen go --in ffcompile.yaml --config ffcodegen.yaml --out featureflags_gen.goGenerate Go accessors with defaults:
go run ./cmd/ffcodegen go --in ffcompile.yamlSupported flags:
--in: required input path--config: optionalffcodegen.yaml--out: optional output path, stdout when omitted or---format:auto,authoring, ornormalized--dump: when reading authoring YAML, also write normalized YAML
When --config is omitted, ffcodegen go uses:
- package:
featureflags - context fields: auto-extracted from
varreferences - accessor names: auto-generated from flag keys
- output: stdout unless
--outis given
Default target values:
| Field | Default |
|---|---|
targets.go.package |
featureflags |
targets.go.context_type |
EvalContext |
targets.go.client_type |
Client |
targets.go.evaluator_type |
Evaluator |
targets.go.context.fields |
auto-extracted from var references |
targets.go.accessors |
auto-generated from flag keys |
Minimal example:
version: v1
source: ./ffcompile.yaml
targets:
go:
package: featureflag
output: ./internal/featureflag/featureflags_gen.go
context_type: EvalContext
client_type: Client
evaluator_type: Evaluator
context:
defaults:
scalar_types:
int: int
collection_types:
int: "[]int"
fields:
- path: user.id
name: UserID
type: int64
- path: device.platform
name: Platform
type: string
accessors:
enable-new-home:
name: EnableNewHome
checkout-mode:
name: CheckoutMode
variant_type: CheckoutModeVariantTop-level fields:
version: must bev1source: source authoring file pathtargets: target-specific generator settings
Go target fields:
targets.go.package: generated package nametargets.go.output: suggested output pathtargets.go.context_type: generated context struct nametargets.go.client_type: generated client interface nametargets.go.evaluator_type: generated evaluator type nametargets.go.context.defaults: optional inferred type overrides for generated context fieldstargets.go.context.fields: optional context field overridestargets.go.accessors: optional accessor and variant type overrides
context.defaults changes the generated Go type used when ffcodegen infers a field from condition operands.
Supported keys:
context.defaults.scalar_types.stringcontext.defaults.scalar_types.boolcontext.defaults.scalar_types.intcontext.defaults.scalar_types.floatcontext.defaults.collection_types.stringcontext.defaults.collection_types.boolcontext.defaults.collection_types.intcontext.defaults.collection_types.floatcontext.defaults.collection_types.any
Example:
context:
defaults:
scalar_types:
int: int
collection_types:
int: "[]int"
string: "[]string"context.fields[].type still takes precedence for per-path overrides.
context.fields overrides the inferred Go field name or type for a path used in conditions.
Each field supports:
path: attribute path such asuser.idname: generated Go field nametype: generated Go field type
Supported field types:
stringboolintint64float64[]string[]bool[]int[]int64[]float64[]anymap[string]any
If a path is referenced in authoring YAML but not listed in context.fields, ffcodegen infers it automatically.
For collection operators, contains and in also infer slice types such as []string and []int64 when the operand shape makes the collection side unambiguous.
accessors lets you override generated names per flag.
Supported fields:
name: accessor namevariant_type: variant enum type name for string-valued flags
The generated Go package is designed around usage, not raw evaluation payloads.
- boolean, string, int64, float64, object, and list variant flags become typed accessor methods
- referenced attributes become a typed context struct
- runtime SDK integration stays outside the generated package
API patterns:
| Flag shape | Generated signature |
|---|---|
| context used, no rollout | Flag(ctx context.Context, ec EvalContext) |
| no context, no rollout | Flag(ctx context.Context) |
| rollout or stickiness required | Flag(ctx context.Context, targetingKey string) |
Typical usage:
evaluator := featureflag.New(client)
enabled, err := evaluator.EnableNewHome(ctx, featureflag.EvalContext{
UserID: userID,
})
variant, err := evaluator.CheckoutMode(ctx, "user-123")For rollout or stickiness based flags, generated accessors take an explicit targetingKey string.
variant, err := evaluator.ExperimentRollout(ctx, "user-123")If a rollout uses a stickiness path such as user.id, the generated evaluator also mirrors targetingKey into that attribute path so providers that bucket on nested attributes can resolve the same identifier.
The generated Go code expects a small client interface implemented by your runtime evaluator.
ffcodegen intentionally keeps this interface SDK-agnostic, so consumer projects can adapt OpenFeature or another runtime without leaking third-party SDK types into generated application code.
Typical setup:
- Generate typed accessors with
ffcodegen - Implement the generated
Clientinterface in your infra layer - Convert the generated
EvaluationContextinto your runtime SDK's context type inside that adapter
For OpenFeature, the intended integration is a thin adapter over *openfeature.Client, not direct OpenFeature types in generated code.