Stop variables export/import from silently corrupting values - #70944
Conversation
`airflow variables export` writes a bare value for any variable without a
description, while `airflow variables import` treats every dict carrying a
"value" key as a {"value": ..., "description": ...} envelope. A variable whose
own value happens to have that shape is therefore unwrapped on the way back in:
its value is replaced by the inner "value" and a description is invented from
the inner "description". Nothing warns the operator, so a round-trip through a
file - the documented way to migrate variables between environments - quietly
rewrites their data.
Export also decodes the stored JSON before writing it out, which erases the
difference between a variable holding raw text and one holding a JSON-encoded
string. Re-importing flattens the latter, and any Dag reading it with
deserialize_json=True starts failing on a value that is no longer valid JSON.
Import's reconstruction is deterministic - strings are stored verbatim,
everything else is JSON-encoded, and one envelope layer is unwrapped - so export
alone can be made lossless. Fixing it there leaves import untouched and keeps
hand-written import files working exactly as before.
92d0c1b to
0aa93cb
Compare
henry3260
left a comment
There was a problem hiding this comment.
The export change looks like it regresses the UI import path for the same two cases this PR fixes for the CLI, since the CLI and UI use different unwrap rules — as far as I can tell there's no export-only change that's correct for both. The PR notes the UI limitation as follow-up, but given export now depends on the unwrap rules, I'd lean towards fixing the UI side in this PR rather than deferring it.
I'm not familiar with the UI part though, so I'd like to hear your thoughts. cc @bbovenzi
Backport successfully created: v3-3-testNote: As of Merging PRs targeted for Airflow 3.X In matter of doubt please ask in #release-management Slack channel.
|
…ues (apache#70944) * Stop variables export/import from silently corrupting values `airflow variables export` writes a bare value for any variable without a description, while `airflow variables import` treats every dict carrying a "value" key as a {"value": ..., "description": ...} envelope. A variable whose own value happens to have that shape is therefore unwrapped on the way back in: its value is replaced by the inner "value" and a description is invented from the inner "description". Nothing warns the operator, so a round-trip through a file - the documented way to migrate variables between environments - quietly rewrites their data. Export also decodes the stored JSON before writing it out, which erases the difference between a variable holding raw text and one holding a JSON-encoded string. Re-importing flattens the latter, and any Dag reading it with deserialize_json=True starts failing on a value that is no longer valid JSON. Import's reconstruction is deterministic - strings are stored verbatim, everything else is JSON-encoded, and one envelope layer is unwrapped - so export alone can be made lossless. Fixing it there leaves import untouched and keeps hand-written import files working exactly as before. * apply suggestion --------- (cherry picked from commit f549b34) Co-authored-by: Y-C <easoneason0905@gmail.com> Co-authored-by: Eason09053360 <185830721+Eason09053360@users.noreply.github.com>
…ues (apache#70944) * Stop variables export/import from silently corrupting values `airflow variables export` writes a bare value for any variable without a description, while `airflow variables import` treats every dict carrying a "value" key as a {"value": ..., "description": ...} envelope. A variable whose own value happens to have that shape is therefore unwrapped on the way back in: its value is replaced by the inner "value" and a description is invented from the inner "description". Nothing warns the operator, so a round-trip through a file - the documented way to migrate variables between environments - quietly rewrites their data. Export also decodes the stored JSON before writing it out, which erases the difference between a variable holding raw text and one holding a JSON-encoded string. Re-importing flattens the latter, and any Dag reading it with deserialize_json=True starts failing on a value that is no longer valid JSON. Import's reconstruction is deterministic - strings are stored verbatim, everything else is JSON-encoded, and one envelope layer is unwrapped - so export alone can be made lossless. Fixing it there leaves import untouched and keeps hand-written import files working exactly as before. * apply suggestion --------- (cherry picked from commit f549b34) Co-authored-by: Y-C <easoneason0905@gmail.com> Co-authored-by: Eason09053360 <185830721+Eason09053360@users.noreply.github.com>
airflow variables exportfollowed byairflow variables importcan silently rewrite a variable's value and invent a description for it.Export writes a bare value when a variable has no description, while import treats any dict carrying a
"value"key as a{"value": ..., "description": ...}envelope — nothing distinguishes the envelope from a value that happens to look like one. Export also decodes the stored JSON, losing the difference between raw text and a JSON-encoded string.Round-tripping through a file on
main:{"value": "a", "description": "b"}{"value": "a", "description": "b"}a, description →b{"value": 1, "other": 2}{"value": 1, "other": 2}1,otherdropped"hello"hellohello— no longer valid JSON, sodeserialize_json=Truestarts raisingImport's reconstruction is deterministic — strings are stored verbatim, everything else is JSON-encoded, and one envelope layer is unwrapped — so export alone can be made lossless, leaving hand-written import files behaving exactly as before. Export now emits the stored form when the decoded value is a string, and wraps values that are themselves envelope-shaped so import's unwrap consumes our envelope rather than the user's data.
Exported files are unchanged for values that were never ambiguous. One visible change worth flagging: a variable stored as a JSON-encoded string (
Variable.set(k, "text", serialize_json=True)) now exports as"\"text\""rather than"text". The old output could not be re-imported without corruption, so there is no lossless way to keep it.Left for follow-up PRs: import still truncates a hand-written
{"value": 1, "other": 2}to1, and the UI import path does not unwrap the envelope at all.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5) following the guidelines