Using ~ (null) in Helm: Deleting Default Values and Handling Optional Fields

You override a chart’s default livenessProbe with an exec command, deploy, and Kubernetes rejects it: “may not specify more than one handler type.” The chart’s default httpGet probe is still there, merged underneath your override, and now the pod spec has two probe handlers. You didn’t add it. You can’t see it in your values file. And the fix is a single character: ~.

That ~ is YAML’s null, and in Helm it does something most people never learn: setting a key to null deletes it from the merged values entirely, instead of setting it to a null value. It’s the cleanest way to remove a default a chart baked in — and it’s the tip of a whole set of null-handling behaviors (absent vs null vs empty, --set foo=null, default, required, hasKey) that quietly decide whether your templates render valid YAML or foo: <no value>.

This guide covers the null-deletes-a-key trick in depth, where it works and where it bites (the --reuse-values trap, the Helm 4 regression), and the related functions you need to tell “the user didn’t set this” apart from “the user set this to empty.” Everything is verified against current Helm docs and behavior.

First: ~ is just YAML null

Before Helm, this is pure YAML. All of these mean the same thing — null:

a: ~        # canonical shorthand
b: null
c: Null
d: NULL
e:          # empty value is also null

~ is the canonical short form in the YAML spec; null/Null/NULL/empty are equivalent spellings. In a Helm values.yaml, writing foo: ~ is identical to foo: null. People reach for ~ because it’s terse and unmistakable — an empty value (foo:) is easy to misread as “I forgot to fill this in,” whereas foo: ~ reads as a deliberate null.

The Killer Trick: null Deletes a Default Key

Here is the behavior that makes ~ worth an article. When you override a chart’s values — with a -f values file, a parent chart overriding a subchart, or --set — Helm merges your values on top of the chart’s defaults. A normal override replaces a value. But a null override is special: Helm removes the key from the result.

Straight from the Helm docs (Chart Template Guide → Values Files):

“If you need to delete a key from the default values, you may override the value of the key to be null, in which case Helm will remove the key from the overridden values merge.”

The canonical example is the liveness-probe foot-gun from the intro. Say the chart defaults to:

# chart's values.yaml
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080

You want an exec probe instead. If you just add your exec, the merge keeps the default httpGet — and a probe with both exec and httpGet is invalid. You delete the default with null:

# your override values.yaml
livenessProbe:
  httpGet: ~          # ← deletes the chart's default httpGet
  exec:
    command: [cat, docroot/CHANGELOG.txt]

Or on the command line, exactly as the Helm docs show it:

helm install stable/drupal \
  --set livenessProbe.exec.command='{cat,docroot/CHANGELOG.txt}' \
  --set livenessProbe.httpGet=null

The result has only the exec handler. Without the httpGet=null, both survive and Kubernetes rejects the manifest.

Why it works: Helm’s value coalescing walks the override tree onto the defaults. A present key with a real value overwrites; a present key with null is treated as an instruction to remove. This is the only way to subtract from a chart’s defaults — there’s no --unset flag (more on that below).

Where the null-delete works

The same mechanism applies anywhere Helm coalesces values:

  • A -f override file on top of the chart’s values.yaml (the docs example above).
  • A parent chart overriding a subchart. In the parent’s values.yaml, nulling a key under the subchart’s name deletes that subchart default:
  • “`yaml
  • # parent values.yaml
  • mysubchart:
  • someDefault: ~ # remove a default the subchart shipped
  • “`
  • --set key=null at install/upgrade time.

--set foo=null vs --set-string foo=null

On the CLI the distinction matters, and it’s a common source of “why didn’t it delete?”:

CommandResult
--set foo=nullfoo becomes a real nil → key is deleted
--set-string foo=nullfoo becomes the literal string "null" (no deletion)
--set a=null,name=[]a: null, name: []

Helm’s --set parser does type conversion: the literal null (case-insensitive) is converted to a Go nil, which triggers the delete. --set-string forces everything to stay a string, so null is just the four-character word "null" — which is almost never what you want here. If your deletion isn’t happening, check you didn’t reach for --set-string.

Version Support (and the Helm 4 warning)

This is where “desde qué versión” gets interesting:

  • Helm 2 introduced deleting a key by setting it to null, but only reliably for top-level keys. Nested null-deletion (like web.livenessProbe.httpGet: null) was buggy — it could emit Cannot overwrite table item ... with non table value and override with null instead of deleting.
  • Helm 3 is where the documented behavior works as advertised, top-level and nested. If you’re on Helm 3, everything above is solid.
  • ⚠️ Helm 4 — currently a regression. As of a still-open issue (filed March 2026), Helm 4 no longer reliably deletes keys via null: both foo: (blank) and foo: null can fail schema validation with errors like Invalid value: "null": ... must be of type string, especially against strict Kubernetes 1.34+ schemas. If you’ve moved to Helm 4, test null-deletion before relying on it — the behavior that was rock-solid in Helm 3 is in flux. This interacts directly with values JSON schema validation: a schema that types a field as string will reject a null, so schema and null-deletion can fight each other.

The --reuse-values Trap

The one place null-deletion does not do what you’d hope: helm upgrade --reuse-values.

--set foo=null deletes a key relative to the chart’s defaults. It does not cleanly “un-set” a value you previously set explicitly and are now carrying forward with --reuse-values — it overrides it with null rather than falling back to the chart default. There is a long-standing feature request for an explicit --unset flag precisely because this case has no clean answer today.

Practical rule: to genuinely reset a value back to the chart default on upgrade, prefer re-specifying your full intended values (-f) over leaning on --reuse-values plus --set x=null.

Absent vs null vs empty: The Trio That Trips Everyone

Deleting keys is half the story. The other half is reading optional values in templates — and Helm/Sprig blur three states that feel different: key absent, key present but null, and key present but empty (0, "", [], {}, false).

The critical thing to internalize: default, required, empty, and coalesce all treat nil, 0, "", empty list/map, and false as the same “empty.” They cannot tell “unset” from “set to zero.”

replicas: 0        # a DELIBERATE zero...
```
```gotemplate
{{ .Values.replicas | default 3 }}   # ...renders 3, not 0 — surprise!

Here’s what each tool actually does:

You want to…UseBehavior
Provide a fallback for empty/unset`{{ .Values.foo \default “x” }}`Returns "x" if foo is nil, 0, "", [], {}, or false
Fail loudly if unset{{ required "foo is required" .Values.foo }}Errors on nil and on empty string (same “empty” rule as default)
First non-empty of several{{ coalesce .Values.a .Values.b "x" }}Skips every empty/null, returns first real value
Tell present-but-null from absent{{ if hasKey .Values "foo" }}The only reliable presence check — true even when the value is null
Safely read a nested optional{{ dig "a" "b" "fallback" .Values }}Walks .a.b, returns "fallback" if any level is missing
Branch on a condition{{ ternary "yes" "no" .Values.enabled }}"yes" if truthy, "no" if empty/false

If you need to honor a deliberate 0 or false, default is wrong — use hasKey to check presence explicitly:

replicas: {{ if hasKey .Values "replicas" }}{{ .Values.replicas }}{{ else }}3{{ end }}

The Rendering Foot-gun: <no value> vs null vs ""

The nastiest null bug isn’t logic — it’s a null leaking into your YAML as a broken string. The same nil value renders three different ways:

foo: {{ .Values.foo }}            # → foo: <no value>   ❌ invalid YAML-ish garbage
foo: {{ .Values.foo | toYaml }}   # → foo: null         ✅ valid YAML null
foo: {{ .Values.foo | quote }}    # → foo: ""           ✅ valid empty string

Bare-printing an unset value gives you the literal text <no value> in the manifest — which is not null, not empty, just a string that will confuse Kubernetes or your reader. (You may also see <nil> in some contexts; the exact literal is a Go-template detail that has shifted across Helm 3 minor versions, so don’t hard-code assumptions about which one appears — the point is it’s not what you want.)

The fixes:

  • Piping through toYaml turns nil into a proper null — use it for whole objects/maps: {{ .Values.config | toYaml | nindent 2 }}.
  • Piping through quote turns nil into "" — use it for optional scalars that must be strings.
  • Best of all, skip the key entirely when empty with with:
  • “`gotemplate
  • {{- with .Values.foo }}
  • foo: {{ . | quote }}
  • {{- end }}
  • “`
  • The with block is skipped for any empty/nil value, so an unset foo produces no line at all — the idiomatic Helm “omitempty.”

Switching Off a Subchart Block with null

One more practical use of ~. Because nil is “empty,” setting a value to null makes an {{ if }} guard fall through:

# subchart default turns something on
metrics:
  serviceMonitor:
    enabled: true
```
```yaml
# parent override switches it off cleanly
metrics:
  serviceMonitor: ~     # or: enabled: false

Nulling the whole block (or the flag) makes {{ if .Values.metrics.serviceMonitor }} evaluate false — a tidy way for a parent chart or an environment override to disable a section a subchart enabled by default, using the same empty-semantics as everything above.

Cheat Sheet

GoalSyntax
Write a null in valuesfoo: ~ (or foo: null)
Delete a chart’s default keyoverride it with ~ / null
Delete via CLI--set foo=null (not --set-string)
Fallback for empty/unset`{{ .Values.foo \default “x” }}`
Distinguish null from absent{{ if hasKey .Values "foo" }}
Require a value{{ required "msg" .Values.foo }}
Nested optional read{{ dig "a" "b" "fallback" .Values }}
Null-safe object into YAML`{{ .Values.obj \toYaml \nindent 2 }}`
Omit a key when empty{{- with .Values.foo }} … {{- end }}

Wrapping Up

~ in Helm is a two-job character: it writes a YAML null, and — the part almost nobody documents in their own charts — it deletes a default key from the merged values, which is the only clean way to subtract a probe, an annotation, or a whole block that a chart shipped by default. Around it sits a set of null rules worth memorizing: default/required/empty can’t tell unset from zero (use hasKey when that matters), and an unset value bare-printed becomes <no value> unless you route it through toYaml, quote, or with.

Two warnings to carry: --reuse-values doesn’t cleanly un-set values, and Helm 4’s null-deletion is currently a regression — so if you’re on 4.x, verify before you rely on it.

For more Helm depth, see the companion guides on values JSON schema validation (which interacts directly with null handling), loading external files into ConfigMaps and Secrets, and what’s new in Helm 4.

Sources