How it works#
You don’t need to know any of this to use helm-schema — but it explains why the schema is more accurate than one derived from values.yaml, and what to expect from the output.
The short version: it reads your chart the way Helm does, follows each value to where it is actually used, and types it from what’s expected there.
It combines three signals#
For every value, helm-schema looks at:
- How your templates use it. Which resource field the value renders into, and the control flow around it (
if,with,range,default,eq,not,or). - Your composed defaults. The root
values.yamlmerged with each subchart’s defaults andglobal— the same composition Helm does. - What Kubernetes expects there. Once a value is traced to a resource field, its type comes from the upstream Kubernetes JSON schema (or the CRD schema) for that field.
These are merged into one Draft-07 schema rooted at the values object. That’s the whole idea — the contract is recovered from what the chart does, not from what its defaults happen to be.
What that buys you#
Compared with reading values.yaml, following the templates catches things a defaults-only tool can’t:
- Values with no default. A value the chart reads but doesn’t ship a default for still appears in the schema, correctly typed.
- Real types, not sample types. A port that happens to be
80isn’t “an integer because 80 looks numeric” — it’s an integer because the Service field it lands in is one. The schema even carries Kubernetes’ own description for that field. - Control flow. A value only used behind
{{ if .Values.x.enabled }}is only constrained when that guard is on — exactly as the template behaves. - Whole subtrees. A block poured into a typed field with
{{ toYaml .Values.resources | nindent … }}expands to the full shape Kubernetes expects there (all oflimits,requests,claims), not just the keys your defaults set. - Typos. The root is closed (
additionalProperties: false), so a key the chart never reads —replicaContinstead ofreplicaCount— is rejected instead of silently ignored.
When it can’t be sure, it doesn’t guess#
Accuracy also means not inventing answers:
- A genuinely ambiguous value stays a union (or an explicit “unknown”) rather than being collapsed to a convenient type.
- Anything that can’t be resolved — a value behind a computed key, an
apiVersionthat can’t be pinned — is surfaced as a diagnostic on stderr, never quietly guessed. For those residual cases you can add a schema override.
Two guarantees worth knowing#
- It ignores any shipped
values.schema.json. A schema a chart or dependency already ships is another author’s assertion — possibly stale, incomplete, or written for a different purpose.helm-schemanever reads it as input; the schema is always recovered from the chart itself. (If you want to inject assertions, that’s what--override-schemais for — applied explicitly, by you.) - The output is deterministic. The same chart, options, and upstream schemas always produce byte-identical output, so it diffs cleanly and is safe to commit and check in CI.