Skip to content

feat: add support for typesafe object flags#248

Draft
beeme1mr wants to merge 1 commit into
mainfrom
feat/typed-object-flags
Draft

feat: add support for typesafe object flags#248
beeme1mr wants to merge 1 commit into
mainfrom
feat/typed-object-flags

Conversation

@beeme1mr
Copy link
Copy Markdown
Member

@beeme1mr beeme1mr commented May 8, 2026

This PR

  • extends the schema to support a simplified JSON schema format
  • extends generators to create typesafe objects
  • adds optional runtime validation

Notes

This is a bit of an experiment. We can scrape the idea, but I think this could be very valuable as the line between feature flagging and dynamic configuration continues to blur.

Follow-up Tasks

I need to double-check that the validators work as expected in all SDKs.

How to test

I've updated all the tests and manually tested the Node.js generator.

Signed-off-by: Michael Beemer <michael.beemer@dynatrace.com>
@beeme1mr beeme1mr requested review from kriscoleman and tomflenner May 8, 2026 12:11
@beeme1mr
Copy link
Copy Markdown
Member Author

beeme1mr commented May 8, 2026

Hey @kriscoleman and @tomflenner, I'm still experimenting with this but I figured I'd get early feedback from both of you.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for an optional 'schema' field on object-type feature flags, enabling type-safe code generation and runtime validation across multiple languages (Go, TypeScript, Java, C#, Python). It includes updates to the CLI command structure to support a '--runtime-validation' flag, adds documentation for the new schema capabilities, and provides comprehensive test coverage for the generated code and validation logic. All review comments regarding missing trailing newlines in golden files and sample manifests have been identified as actionable improvements and are being addressed.

return new GeneratedClient(Api.Instance.GetClient(domain));
}
}
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a good practice to end files with a newline character. Please add a newline at the end of this file.

}
}
}
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a good practice to end files with a newline character. Please add a newline at the end of this file.

return client.getObjectDetails("themeCustomization", {"header":{"fontSize":16,"visible":true},"primaryColor":"#007bff","secondaryColor":"#6c757d","tags":["light"]}, context, { ...options, hooks: [...(options?.hooks ?? []), createThemeCustomizationHook()] }) as Promise<EvaluationDetails<ThemeCustomization>>;
},
}
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a good practice to end files with a newline character. Please add a newline at the end of this file.

}
}
}
} No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a good practice to end files with a newline character. Please add a newline at the end of this file.


By default, object flags return generic types in generated code (`any` in Go, `JsonValue` in TypeScript, `object` in Python, etc.). This forces developers to manually cast values and provides no compile-time safety.

With the `schema` field, generated code includes:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😎


## Runtime Validation

When code is generated with `--runtime-validation` (enabled by default), the CLI generates OpenFeature `after` hooks that validate the provider's response at evaluation time. This catches cases where a feature flag provider returns an object that doesn't match the expected schema.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little iffy about "runtime-validation" simply because it doesn't actually run at runtime, right?
Maybe simply type-validation or validation? 🤔
Considering the fact that this runs after generation and before runtime use.

baseType = "bool"
case "array":
if schema.Items != nil {
baseType = "List<" + csharpSchemaType(schema.Items, true, parentName+"Item") + ">"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use an IEnumerable or IList here, for better C# SOLID practice

if schema.Properties != nil {
baseType = parentName
} else {
baseType = "Dictionary<string, object>"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to above, I think it'll be more C# idiomatic here to use IDictionary<TKey, TValue>

@kriscoleman
Copy link
Copy Markdown
Collaborator

[REVIEW AGENT] Automated code review — major findings and positive observations below.


Major Findings

1. C# DetailsAsync returns FlagEvaluationDetails<Value> instead of the typed record

ThemeCustomizationAsync correctly returns Task<ThemeCustomization>, but ThemeCustomizationDetailsAsync is hardcoded to return Task<FlagEvaluationDetails<Value>> — the raw, untyped OpenFeature Value. This defeats the purpose of typed code generation for the details variant: callers get back an untyped Value and have to deserialize it themselves.

The Java generator already handles this correctly by deserializing into the typed record and returning a fully typed FlagEvaluationDetails<ThemeCustomization>. The C# details method should follow the same pattern.


2. Go: json.Marshal / json.Unmarshal errors silently discarded

The generated Go code does:

b, _ := json.Marshal(raw)
json.Unmarshal(b, &result)

Both error return values are discarded. A marshal failure (e.g. a non-serializable type from the provider) or unmarshal failure (type mismatch) would silently return a zero-value struct — a subtle, hard-to-debug failure mode.

For ValueWithDetails, which already returns error, the errors should be propagated:

b, err := json.Marshal(raw)
if err != nil {
    return openfeature.GenericEvaluationDetails[ThemeCustomizationValue]{}, fmt.Errorf("marshal: %w", err)
}
if err := json.Unmarshal(b, &result); err != nil {
    return openfeature.GenericEvaluationDetails[ThemeCustomizationValue]{}, fmt.Errorf("unmarshal: %w", err)
}

For the Value variant (no error return), at minimum log or panic with a descriptive message rather than silently swallowing the failure.


Positive Observations

  • Backward compatibility is airtightschema is optional at every layer; manifests without schemas produce identical output to before.
  • Cross-language coverage is thorough — all 8 language targets addressed with golden file tests.
  • --runtime-validation defaults to true — opt-out rather than opt-in is the right default; users get safety without having to ask for it.
  • Error messages in validation hooks include the full dotted property path (e.g. themeCustomization.header.fontSize), making failures immediately actionable.
  • Default values are validated against the schema at manifest load time — catching mismatches at codegen rather than at flag evaluation runtime is exactly the right place.
  • SortedPropertyNames usage ensures deterministic output ordering, essential for stable golden file testing.
  • docs/object-flag-schemas.md is comprehensive — covers supported keywords, per-language behavior differences, known limitations, backward compatibility, and copy-pasteable examples.

Copy link
Copy Markdown
Collaborator

@kriscoleman kriscoleman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty awesome so far!

I left a couple notes; one regarding the runtime-validation flag and another regarding Dependency Inversion Principle best practices for c#.

For other typed languages (typescript, java) I would also suggest reviewing the type matching and ensuring that any collection types (arrays, dicts) are using interfaces and not concretions.

I also dispatched a review agent which found a couple more nits.

Aside from that, it's looking great! 🥇 Really great improvements here, nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants