Apache Spark WTF??? ✈️ Come Fly With Me🧑✈️
Welcome to the airport. Keep your boarding pass close and your schema assumptions closer. Spark 4 just opened a new terminal for JSON, XML…

“Come fly with me, let schemas drift away
If your JSON gets exotic, there’s a nested array”
(adapted from “Come fly with me” by Frank Sinatra)
🛫 Departure Board: Why Semi-Structured Data Became Everyone’s Problem
For years, analytics lived in a beautiful fantasy of clean data, stable columns, obedient types, and upstream systems that supposedly read the API documentation. That world still exists in dashboards and PowerPoint diagrams where every arrow is blue, but operational data is far less polite.
To stress-test this reality, we’re using airline commercial data. A flight offer isn’t a neat, flat row. It’s a chaotic burrito of routes, legs, passengers, fare brands, baggage rules, refund penalties, and nested supplier surprises wrapped in JSON, XML, or both.
At the easy end, an offer like MAD->BCN is the data equivalent of traveling with just a backpack: one passenger, one cabin, one price, and zero drama. But reality usually shows up wearing noise-canceling headphones. A multi-leg trip like MAD->DOH->SIN brings ordered legs, multiple carriers, mixed cabins, loyalty statuses, infant passengers, meal preferences, taxes, and supplier extensions nested so deeply they need their own postal code.
Routes contain legs, prices contain suspiciously cheerful totals, and events contain payload versions and status changes. This complexity makes airline data the perfect arena to test Spark 4’s semi-structured capabilities against production-shaped reality instead of pretending a tiny toy object represents a real pipeline.
The Schema Philosophy & Enter Spark 4
The classic data warehouse instinct is schema-on-write: define the structure, reject outliers, and sleep like a normal person. That works great until a supplier stores a nested emotional biography inside departure_airport. Airline payloads demand schema-on-read for API evolution and partner integration—you preserve the raw shape, inspect it, and promote meaning only when the business question becomes clear.
The real debate isn’t “schema or no schema”.
It’s unknown today versus flexible forever.
- Unknown today means discovering, validating, and eventually promoting data into typed columns.
- Flexible forever means keeping supplier extensions and commercial doodads that were clearly not invited to the architecture meeting.
Spark 4 upgrades this messy middle zone with two massive tools: built-in XML support for legacy payloads and the new VARIANT type for JSON-like semi-structured data. Neither replaces schemas; they simply provide a better holding area before the data earns a permanent silver seat.
The Engineering Thesis
Let’s be incredibly clear: VARIANT does not replace schemas. Claiming that would be nonsense wearing a pilot hat. Typed columns, contracts, query plans, storage layouts, and the Spark UI (which will absolutely expose your computational crimes) still matter.
The thesis is simpler: VARIANT helps Spark handle semi-structured data honestly. It preserves nested, evolving JSON payloads without immediately flattening them into relational sadness or marooning them as opaque strings. It’s a bridge between raw ingestion and disciplined modeling—but a bridge is not a destination.
In a serious architecture:
- Raw payloads are preserved for audit and replay.
- Semi-structured payloads become navigable.
- Stable fields are promoted into typed columns.
- Broken records are quarantined.
- Supplier drift is measured.
- Business rules are tested.
Dashboards shouldn’t rely on twenty-seven fragile path extractions just because nobody designed the silver table. The question isn’t whether to use JSON, XML, or VARIANT. The question is: What must be preserved, queried, validated, and kept flexible?
Spark 4 doesn’t remove these decisions; it makes them harder to avoid.
🧪 Lab 1: The Tiny Airport Dataset
To kick things off, lab_01__the_tiny_airport_dataset.ipynb creates a synthetic, sub-1 MB dataset. It writes identical flight offers across JSON Lines, XML, typed Spark DataFrames, and CSVs with embedded JSON. We aren’t benchmarking performance yet; we are strictly checking semantic equivalence across origin, destination, carrier, fare family, and total price to ensure our data survives the journey.
🛂 Security Gates: What Spark Used to Do with JSON Before VARIANT
Before Spark 4 introduced the VARIANT boarding lane, semi-structured data had to pass through three old-school security gates. You could keep JSON as a string, parse it with an explicit schema, or let Spark infer the schema and cross your fingers. All three are valid, but they each fail in their own special way.
Remember, JSON is not magic; it’s just text. Before Spark understands fields, arrays, or nested commercial chaos, that text must be parsed. That parsing step is exactly where small engineering choices mutate into future compute bills and late-night debugging sessions.
JSON as String: The Suitcase Nobody Wants to Open
The easiest trick is also the most tempting: dumping a supplier’s API payload into a STRING column. Boom—bronze layer landed, demo survived.
Raw strings are fantastic for ingestion and replay because they act as an undisputed black box after a pipeline crash. If a supplier swears they never sent a weird baggage rule, the raw payload has the receipts to silently judge everyone.
But as analytical furniture, raw strings are awful. Want the cheapest flight with baggage? You’re digging through text, extracting, casting, and praying for every single query. Functions like get_json_object can inspect paths, but invalid JSON simply returns a silent NULL. A missing field, a path typo, and a failed cast all collapse into the exact same suspicious silence.
In the airline world, missing baggage data isn’t the same as excluded baggage. Keep raw JSON for auditing, but don’t use it as your main analytical interface unless you enjoy silent data tragedies.
from_json: The Strict Airport Officer
The strict gate is from_json: hand Spark a string and a schema, and it builds proper StructType, ArrayType, or MapType shapes. Suddenly, your payload makes sense, validation is a breeze, and totals become actual money instead of mysterious string souvenirs.
This is perfect for stable contracts near production because a schema explicitly states what you believe the payload means. The problem? Commercial airline data hates agreeing on anything. One supplier sends baggage as an object; another uses an array. Refundability might be a clean boolean here, but a nested penalty structure there.
If you use strict schemas, you have to nail down the shape early, which is incredibly painful when you’re still discovering how a new supplier behaves. Teams usually panic by overcorrecting into a massive cathedral of nullable columns, or they build a narrow happy-path schema that blindly drops weird records into the abyss. Use strict schemas for trusted silver outputs, but don’t confuse needing reliable columns with understanding every edge case on day one.
Schema Inference: A Helpful Flashlight, Not Airport Law
The third option lets Spark infer the schema based on what it observes. This is a fantastic flashlight for your first “what on earth did this API send us?” notebook.
However, inference is not a governance department — it only knows what it sees. If the sample lacks infants or rare refundable fares, those branches vanish. If a supplier extension appears in 0.1 percent of records, the inferred schema confidently pretends it doesn’t exist. Add type drift (like a price changing from a number to text), and inference can only tell you what happened, not what the business meaning should be.
In airline terms, inference is the gate agent looking inside a suitcase and saying they found three shirts and something that might be cheese. Use it as starting material to inspect payloads across suppliers and dates, then decide what deserves a strict model, what should be quarantined, and what belongs in a flexible VARIANT column.
🧪 Lab 2: Three Old-School JSON Techniques
In lab_02__three_old_school_json_techniques.ipynb, we run our tiny airport dataset through these three pre-VARIANT gates. By deliberately injecting missing baggage, text-based prices, and malformed JSON, we expose the core trade-offs. Raw strings are flexible but brittle, strict schemas are clean but demand trusted shapes, and inference is a great guide right up until the sample forgets the truth.
🤵♂️ VIP Lounge: Enter VARIANT
Every data platform needs a dedicated room for payloads that aren’t necessarily malicious, but definitely aren’t simple. In Spark 4, that room is VARIANT.
Before now, semi-structured data was forced down two awkward lanes: stay as an opaque, blind string forever, or get shoved into a strict schema too early. One is flexible but blind; the other is clean but impatient. VARIANT provides the ultimate business-class seat: semi-structured data that stays flexible while remaining completely queryable.
It’s the perfect middle ground for payloads that have an inherent structure, but lack the stability to deserve a full schema at the gate.
Why VARIANT Was Needed & What It Solves
The old ways forced engineering teams to pick the wrong trade-offs too early. Storing raw JSON preserved everything, but Spark only saw text — meaning every query required expensive path extractions, casts, and a high probability that missing fields or malformed data would collapse into a silent NULL. On the flip side, schema inference was merely a flashlight describing a small sample, not a strict contract.
VARIANT exists for that volatile middle stage of data maturity—when a payload is real enough to parse, query, and profile, but not mature enough to freeze into a rigid silver schema.
A flight offer contains nested objects, leg arrays, taxes, and supplier weirdness hiding in the overhead bin. VARIANT lets Spark represent these JSON-like values as native, semi-structured data. Suppliers can inject private extensions or experiment with new product bundles without triggering immediate schema surgery on your pipelines.
To be clear: VARIANT is not an excuse for “no schema.”
“No schema” isn’t architecture; it’s how data lakes turn into haunted storage units. The modern approach is progressive structure: preserve the raw payload, parse it into VARIANT to make it navigable, promote stable fields into typed columns, and leave volatile extensions in a flexible sidecar column.
parse_json 🆚 from_json
Tape this distinction directly to the cockpit door:
from_jsonmeans: “I know exactly what this payload should be.”
It takes a JSON string plus an explicit schema and produces strongly typed structures (structs, arrays, maps). This is excellent when your upstream contract is rock-solid.parse_jsonmeans: “I know this is JSON, but I do not trust its shape yet.”
It transforms the text into aVARIANTtype without requiring an upfront schema definition. This is a lifesaver when suppliers disagree—like when one sends a fare family as a string, another as an object, and a third as an array.
For messy pipelines, Spark 4 also gives us try_parse_json. Instead of throwing a runtime exception and crashing the entire cluster when it encounters malformed JSON, it calmly returns a SQL NULL. This allows you to smoothly redirect and quarantine bad records instead of turning your production environment into a crime scene.
The VARIANT Function Family
Once a payload boards as a VARIANT, Spark provides a robust toolkit to query it without pretending it’s a perfect, flat struct:
variant_get: Extracts a specific path and casts it to a target type. If the path is missing it returnsNULL, but if the cast fails (like trying to turn the string “cheap-ish” into a decimal price), it throws an error. Use this when bad data needs to be loud.try_variant_get: The calmer sibling. If the path is missing or the cast fails, it silently returnsNULL. It is ideal for initial data profiling, onboarding, and drift analysis.schema_of_variant&schema_of_variant_agg: The first inspects a single variant value, while the second aggregates and merges observed shapes across millions of rows. These are phenomenal flashlights for detecting upstream supplier drift.variant_explode: Opens up nestedVARIANTarrays or objects into distinct rows. This is crucial for airline data, which natively lives in nested arrays of route legs, passengers, taxes, and baggage rules.is_variant_null: Detects an explicit JSONnull. In serious data engineering, a SQLNULL(path missing), a failed cast, and an explicit JSONnull(value intentionally sent as null) are completely different business events. Missing baggage data is not the same as baggage explicitly excluded.
Where VARIANT Belongs in the Lakehouse
The goal is never to replace your entire data model with VARIANT. The safest architecture follows a Raw ➜ Navigable ➜ Promoted pattern:
- Bronze (Raw + Navigable): Store the pristine, unaltered raw string payload for audit compliance and replayability. Right beside it, store the parsed
VARIANTcolumn. This creates an immediately navigable, semi-structured layer for profiling and extraction without forcing you to repeatedly parse raw text downstream. - Silver (Promoted): Extract and promote stable, high-value fields into typed columns (e.g.,
booking_id,origin,destination,total_amount). If a field drives critical business logic, filters, joins, or dashboards, it must earn a permanent, typed seat. - The Sidecar: Keep a
VARIANTcolumn running alongside your typed Silver columns to hold volatile supplier extensions, experimental metadata, and localized attributes that are still evolving.
🧪 Lab 3: The First VARIANT Boarding Pass
In lab_03__the_first_variant_boarding_pass.ipynb, we parse our synthetic airline payloads into VARIANT while keeping the raw text intact. We walk through progressive extraction, safely handle type drift and missing paths, explode complex leg arrays, and isolate malformed payloads using try_parse_json. The lab demonstrates how VARIANT serves as a powerful exploration and controlled-promotion layer—not a lazy replacement for disciplined data modeling.
🧳 Baggage Claim: Nulls, Missing Fields, and Other Little Betrayals
Welcome to baggage claim: the place where payloads have technically arrived, but nobody is emotionally ready to discuss their condition. Semi-structured data looks fine from a distance. Events register, payloads parse, and dashboards glow green. Deep inside, however, business-critical fields may be missing, explicitly null, malformed, or shaped like a small XML souvenir from 2009.
SQL Null 🆚 VARIANT Null
Data engineers say “null” as if it were one thing. It is not. In semi-structured pipelines, missing paths, explicit JSON nulls, invalid formats, wrong path expressions, and failed casts all collapse into the same suspicious silence. In reality, these are entirely different business events.
Consider baggageAllowance in an airline payload:
- Missing key: The supplier doesn’t send baggage data in this specific API version.
- Explicit
"baggageAllowance": null: Baggage was evaluated by the supplier but is explicitly unavailable. - Empty array
[]: Zero checked bags are included. - Quantity of
0: Bags are explicitly excluded.
Treating all of these as the same generic NULL blob is how you end up telling an airport agent “no bag allowed” when the real answer was “the supplier didn’t tell us.”
VARIANT makes these boundaries visible, but it won’t handle them automatically. Use is_variant_null to identify explicit JSON nulls. Remember that a missing path extracted via variant_get or try_variant_get returns a standard SQL NULL. Classify your missing data deliberately—missing fields, explicit nulls, and failed casts are all critical evidence. Don’t sweep them into the same trash bin just because they are inconvenient to model.
Optional Fields Are Not Bad Data
Traditional database schemas often treat optional fields like they are defective, as if all real data secretly wanted to be a textbook dimension table. But airline data is full of valid, conditional structures.
A traveler might have loyalty status. A fare might include refund penalties. A direct route won’t have layover metadata because there is no layover. Meal preferences, corporate contracts, and lounge access only appear for certain cabins, markets, or haul lengths. That isn’t bad data — that’s business logic.
JSON represents these optional branches naturally, and VARIANT preserves them without forcing your architecture into a giant, nullable cathedral of columns.
However, optional doesn’t mean ungoverned. A serious pipeline defines what is mandatory (e.g., origin and destination for a route search), what is conditionally required (layover duration for multi-leg itineraries), and what is genuinely volatile (supplier extensions).
The dangerous shortcut is turning uncertainty into a clean, lazy boolean. Flagging baggage_included = false looks tidy, but it is dangerously confident if the payload simply lacked baggage information entirely. Sometimes the correct business answer isn’t true or false; it’s: we do not know, and we know exactly why.
Type Drift: When Price Becomes a String Because Tuesday
Type drift happens when a field keeps its name but changes its costume. Yesterday, totalAmount was a decimal. Today, it’s "149.99". Tomorrow, it’s 14999 with implied cents. By Friday, a legacy supplier sends "N/A" because price has apparently become a lifestyle concept.
Semi-structured pipelines face this constantly: numbers disguised as strings, booleans sent as "Y" or "N", and objects turning into arrays the moment a second item is added.
VARIANT preserves this flexible input without destroying ingestion evidence. Use strict variant_get when bad types should fail loudly, and try_variant_get when you want to safely profile drift or route anomalies into a diagnostics table. But if a field like price is business-critical, it cannot remain a mystery forever.
The production pattern remains unglamorous but necessary: accept the flexible input, preserve the raw evidence, profile the type drift, promote trusted typed outputs, and quarantine records that defy interpretation.
🧪 Lab 4: The Baggage Null Torture Test
In lab_04__the_baggage_null_torture_test.ipynb, we mutate baggageAllowance across missing keys, explicit JSON nulls, empty arrays, weird shapes, and invalid quantities. The lab compares strict from_json parsing with VARIANT inspection to show how to classify each case before promoting clean columns. Clean business columns come after null, optionality, and type-drift diagnostics—not before.
🛋️ Waiting Room: XML Was Not Dead, It Was Just Boarding Late
XML did not die. It merely sat in the enterprise waiting room with a neck pillow, a corporate integration contract, and the terrifying calm of something that remains deeply mission-critical.
Every few years, someone confidently declares XML extinct. Then, a global distribution system, payment platform, government interface, or airline ticketing hub quietly produces an API payload filled with attributes, namespaces, and validation rules from the era of heavy, door-stop laptops.
Spark 4 finally treats XML as a first-class passenger. It isn’t glamorous or trendy, but it is incredibly useful.
Why XML Still Exists & Native Support in Spark 4
It is easy to mock XML for its noisy angle brackets and alphabet-soup namespaces. But XML survived because it solved serious enterprise problems: explicit structure, native metadata attributes, strict validation schemas (XSD), and incredibly long-lived contracts.
In airline commerce, systems were certified, versioned, extended, and then very carefully not replaced because the business logic just works. Modern pipelines cannot pretend XML is ancient archaeology. JSON offer streams frequently live right beside XML order logs, and Kafka topics routinely carry legacy XML payloads shoved inside string columns.
Before Spark 4, handling XML meant wrestling with external libraries or facing an unpleasant date with User Defined Functions (UDFs) and regret. Spark 4 fixes this by adding native XML read and write support directly through the standard DataFrame API.
The most critical parameter you’ll configure is rowTag. Because XML is a tree and a DataFrame is flat, Spark needs to know exactly which repeated XML element constitutes a single row. This single configuration defines your entire analytical universe:
- Choose
<Offer>, and each row describes shopping behavior. - Choose
<Booking>, and each row tracks order-level states. - Choose
<FlightSegment>, and you are analyzing individual legs rather than full itineraries.
Pick the wrong rowTag and Spark will still cheerfully build a DataFrame—it will simply be the completely wrong DataFrame.
XML Functions Inside Columns
XML doesn’t just show up as clean files; it frequently hides inside database columns named payload_text or legacy_blob_do_not_touch. To address this, Spark 4 introduces dedicated column functions:
from_xml: Parses an XML string column into a strongly typed struct based on a provided schema. It is the XML cousin offrom_json, ideal when your data is already inside a DataFrame and the schema is known.schema_of_xml: Infers a DDL-style schema from a sample XML string. It’s a fantastic flashlight for your initial exploration notebooks.to_xml: Serializes a Spark struct back into an XML string. This is crucial for outbound partner integrations, data generation, and round-trip payload testing.
XML Traps Waiting Near the Gate
XML is highly structured, but that does not make it automatically tabular. Watch out for these four distinct traps:
- The Attribute Trap: Carrier codes, currency markers, and tax IDs often live inside XML attributes rather than child elements. Spark can preserve these by applying a configurable prefix (like
_or@) to prevent name collisions with nested elements. Losing attributes during ingestion isn’t simplification—it’s quiet sabotage. - The Array Trap: If schema inference only looks at a polite sample where every flight has one passenger and one tax item, it will generate a flat struct. The moment production data arrives with five passengers and twelve tax breakdowns, your pipeline will truncate data or fail. Ensure arrays are explicitly typed.
- The Namespace Trap: Namespaces prevent tag collisions across different systems, but they can make path names look like they were assembled during severe turbulence. Spark allows you to ignore prefixes for child elements, but use this carefully: simplifying names is great, but accidentally collapsing two distinct business meanings is catastrophic.
- The Corrupt Record Trap: XML can be malformed, truncated, or badly encoded. Spark offers
PERMISSIVE,DROPMALFORMED, andFAILFASTmodes. Choosing your parsing mode is a business decision: in ticketing and payment data, “we silently dropped the weird rows” quickly translates to “we lost our most expensive transactions.”
Respect XML twice: once as a syntax parser to keep the cluster alive, and once as business data to ensure you don’t accidentally sell a flight with an imaginary suitcase and a refund policy written by ghosts.
🧪 Lab 5: The Legacy Partner Terminal
In lab_05__the_legacy_partner_terminal.ipynb, we test Spark 4’s native XML engine against production-shaped airline payloads. We walk through reading raw XML files, parsing XML embedded inside columns, inferring schemas, and validating records against XSD constraints. The lab demonstrates how to cleanly isolate syntax parser failures from actual business rule violations, proving that while XML is now much easier to load, disciplined schema design and quarantine practices still matter.

An airline safety-card guide to boarding semi-structured data in Spark 4, from schema fantasies and old JSON gates to VARIANT, null handling, type drift, and XML still calmly waiting in the enterprise lounge.
🍟 Appetizers at Cruising Altitude: JSON, XML, and VARIANT Side by Side
Now that everyone is seated, the snack tray arrives: JSON on the left, XML on the right, and VARIANT in the middle, looking suspiciously comfortable for something new to Spark 4.
Our core business object is the flight offer: a nested ecosystem of origins, destinations, legs, passengers, fare families, baggage permissions, loyalty eligibility, and supplier extras hiding overhead. Depending on your choice of wrapper, these structures can either streamline ingestion and exploration or make you whisper why at a Spark physical plan at 1:17 a.m.
Same Business Object, Four Passports
Imagine one canonical, long-haul offer: Madrid to Singapore (MAD->DOH->SIN). It features two legs, one economy passenger with loyalty status, a specific fare family, a total price, one checked bag, and conditional lounge access.
The format you use determines how Spark interacts with this exact same offer:
- Strongly Typed Columns: The traditionalist approach. It provides rigid, stable types optimized for dashboards, joins, and high-performance Parquet tables.
- Raw JSON String: Portable, ubiquitous, and the default language of modern APIs and Kafka streams. However, if Spark only sees an opaque string, you must manually parse, extract, and cast fields for every downstream query.
- Native XML: Verbose and tag-heavy, but incredibly contract-friendly. Elements handle strict hierarchal structures, attributes manage metadata, and namespaces preserve enterprise partner semantics.
VARIANTColumn: Native semi-structured data inside Spark. It remains queryable without forcing you to declare every single nested branch upfront. It acts as an exploration bridge, not the final relational destination.
Formats are not architecture. Architecture is what you preserve, parse, validate, promote, and serve. A typed struct can be a poorly designed model, just as VARIANT can either be a disciplined staging layer or a shiny, ungoverned junk drawer. The format won’t save us; only the design will.
When to Use Each Tool
The pattern for a resilient architecture is straightforward: raw data for preservation, parsed structures for navigation, typed columns for trusted business use, and a quarantine bucket for malformed payloads.
- Use Typed Structs when the contract is stable and mature. Fields driving core logic, filter criteria, or financial reporting — like
origin,destination,total_amount, andcurrency—deserve dedicated, typed seats. - Use JSON Functions (
get_json_object) strictly for legacy scripts or quick debugging in an ad-hoc notebook. Building an entire enterprise pipeline on string path extractions is an invitation for silent nulls caused by hidden typos. - Use
from_jsonwhen upstream APIs are rock-solid, version-controlled, and reliable. Spark parses the text into proper structs or maps, and downstream validation becomes clean and predictable. - Use
VARIANTwhen JSON-like payloads are evolving, containing supplier-specific extensions or type drift. It keeps ingestion fluid while letting you gradually extract and promote stable fields over time. - Use Native XML whenever an enterprise transport or GDS contract demands it. Spark 4 native support eliminates external connector headaches, but you must still rigorously configure your
rowTag, manage attributes, and handle repeated element arrays.
Do Not Benchmark Nonsense
This is the exact moment someone opens a laptop and confidently proclaims, “Let’s prove which format is faster.” Stop. You cannot fairly compare raw JSON string parsing, optimized Parquet typed columns, native XML parsing, and VARIANT access using one vague query and a stopwatch. That isn’t engineering; it’s astrology with cluster resources.
Before making performance claims, you must explicitly define the workload. Are you measuring ingestion throughput, storage footprint, repeated extraction latency, memory pressure, or CPU overhead? Furthermore, you must test against realistic business queries — such as finding the cheapest route, filtering by baggage availability, or isolating malformed supplier records.
No performance claim without a workload. No workload without an explicit business question. No business question without checking the Spark UI execution plan. And no execution plan without confirming every format actually returned the same commercial truth. Otherwise, you aren’t benchmarking — you’re just arguing with snacks.
🧪 Lab 6: Same Offer, Different Passports
In lab_06__same_offer_different_passports.ipynb, we create one canonical long-haul flight offer across four formats: typed Spark columns, raw JSON, XML, and VARIANT. We execute identical business questions against all four variants, comparing query complexity, failure behaviors, and underlying physical execution plans. The lab proves a fundamental rule of data platforms: performance metrics only matter after every format successfully arrives at the exact same truth.
🌍 Boarding Groups: Growing the Dataset from Toy to Airline-ish
A tiny dataset is adorable. It fits neatly in memory, behaves beautifully in screenshots, and makes every parsing function look incredibly elegant. While a micro-dataset can easily prove that JSON, XML, typed structs, and VARIANT preserve the exact same business meaning, it tells you absolutely nothing about parsing pressure, file layout, nested array explosions, memory footprint, partition skew, or supplier drift.
To bridge this gap, we grow our dataset carefully. We aren’t doing multi-terabyte disaster tourism just for the sake of massive numbers; instead, we scale using disciplined data boarding groups: Small, Medium, and Mean.
Progressive Dataset Sizes & Synthetic Dimensions
Our dataset scales through four distinct, highly reproducible presets:
- XS (Under 1 MB): Small enough to print, inspect, and explain in a single ad-hoc cell. It is designed purely for semantic checks and code correctness — not performance.
- S (5 to 10 MB): Introduces more airports, mixed carriers, long-haul legs, and optional attributes. It’s still friendly on resource usage, but lazy architecture assumptions start to wobble here.
- M (50 to 100 MB): The laptop pressure point. This is where repeated string extractions, heavy XML parsing, nested arrays, and complex Spark physical-plan overhead become highly visible.
- L (500 MB+): The cluster test. This is where distributed storage, executor memory limits, shuffle stages, partition balance, and data skew begin acting like passengers with severe complaints.
These sizes provide strict reproducibility. Because they share the exact same generator seed, business rules, and formats, readers can easily rerun these experiments on their own laptops or cloud clusters without their emotional support cloud bill blowing up.
Furthermore, our data factory doesn’t just generate random JSON confetti; it outputs production-shaped airline dimensions.
- Routes & Carriers: Distances classify short-, medium-, and long-haul trips, which inherently dictate cabin availability, meal rules, and lounge eligibility. Marketing and operating carriers are split to reflect actual code-share agreements.
- Passengers & Baggage: Travelers are segmented by profiles (e.g., Corporate, Infant, Elite Loyalty). Baggage is deliberately randomized to appear as a missing key, an explicit null, an empty array, a structured piece allowance, or legacy supplier strings like
"1PC"or"23KG". - Financials & Suppliers: Prices include base fares, taxes, surcharges, and currency variables — because money is not a float wearing sunglasses. To replicate real-world ingestion ugliness, specific legacy supplier profiles inject naming drift or wrap JSON structures inside flat CSV files. Fake data is fine, but fake behavior should feel uncomfortably real.
The Nested Complexity Knobs
While dataset size controls raw volume, structural complexity controls psychological pain. We keep those two knobs completely separate using six distinct complexity levels:
- Level 1 (Direct Core): One passenger, one leg, one cabin, one fare, and no extensions. This serves as our baseline for structural equivalence.
- Level 2 (Group & Ancillary Structs): Multiple passengers, baggage tracking per traveler, and optional add-ons like seat assignments or meals. This tests how Spark handles arrays without losing parent offer identity.
- Level 3 (Multi-Leg Connection): Introduces multi-leg routes (e.g.,
MAD->DOH->SIN) where flight legs must chronologically connect and layovers exist. This is wherevariant_explodefinally earns its boarding pass. - Level 4 (Mixed Class & Codeshares): One leg may be economy, the next business. One airline sells the ticket while a partner operates the aircraft. Flat schemas begin sweating into the safety card.
- Level 5 (Supplier Drift Matrix): Upstream field names begin changing costume.
fareFamilyalternates tofare_family, thenbrand. Total amounts alternate between decimals, string text, and missing values. This actively tests your schema-on-read normalization strategy. - Level 6 (The Broken Ingestion Terminal): The ultimate stress test. It floods the pipeline with malformed JSON, truncated XML, negative prices, duplicate events, out-of-order updates, and records that parse perfectly on a syntax level but make zero business sense.
A good engineering lab combines these knobs dynamically: XS Level 1 teaches, S Level 3 validates, M Level 5 pressures, and L Level 6 humbles. That is the fundamental difference between a quick product demo and a rigorous test harness. A demo shows that a technology can work; a test harness shows exactly where it starts lying to you.
🧪 Lab 7: The Synthetic Airline Data Factory
In lab_07__the_synthetic_airline_data_factory.ipynb, we construct our deterministic, seed-based data generator. The script generates repeatable, production-shaped payloads across JSON Lines, XML, embedded CSVs, typed Parquet, and raw-plus-VARIANT layouts. It also exports an explicit manifest tracking row counts, drift categories, and expected validation failures. This ensures all subsequent labs execute against a known, audited truth rather than unpredictable data confetti.
✈️ In-Flight Re-routing: Dynamically Querying Routes, Fares, and Itineraries with VARIANT
We have reached cruising altitude. The synthetic airline factory is loaded, telemetry is flowing, and the commercial desk drops a classic urgent request: find the cheapest Madrid-to-New-York trip next weekend that includes checked baggage, keeps connections under three hours, and places the long-haul leg in business class.
Suddenly, semi-structured payloads must stop being charmingly complex nested objects and start answering raw product questions. This is where VARIANT moves from an ingestion helper to an active, queryable database layer—extracting fares, exploding route legs, validating connection windows, and auditing cabin changes without pretending every upstream supplier agreed on a unified schema.
Direct Route Queries
We begin with the safest path: a direct, non-stop MAD->JFK offer with no interline chaos. To answer basic shopping queries, our pipeline needs a normalized view containing the origin, destination, marketing carrier, cabin, total price, currency, and baggage status.
This scenario highlights the operational split between raw strings and VARIANT:
- Raw JSON Strings: Every single filter and extraction requires repeated, regex-like character poking using
get_json_object. Spark treats the entire record as one heavy character blob, re-parsing the text path by path. VARIANTColumns: The payload is already sliced into native, semi-structured fragments. Spark navigates nested keys directly without text-parsing overhead.
However, agility should not breed laziness. If attributes like origin, destination, or total price drive downstream dashboards, partition layouts, core joins, or customer-facing search APIs, promote them into typed Silver columns. VARIANT is an excellent tool for structural fluidity and rapid data discovery; it is not a lifetime prison for high-velocity fields that the business asks about every fifteen minutes.
Multi-Leg Route Traversal
Now the weather gets turbulent. A flight itinerary is rarely a flat row; it is an ordered array of legs (e.g., MAD->DOH->SIN or JFK->LHR->CDG->FCO). Each leg carries its own carrier codes, localized timestamps, equipment configurations, and cabin classifications. To answer itinerary questions, we must traverse, explode, order, and reconnect these segments without losing track of the parent offer.
This is where variant_explode earns its boarding pass. It projects nested arrays or objects into distinct rows while preserving the structural position (pos), key, and value. For flight routing, position is absolute business truth: MAD->DOH->SIN is structurally and commercially distinct from SIN->DOH->MAD, no matter how relaxed your sorting function feels.
Once exploded, the data pipeline can programmatically audit:
- Leg Continuity: Does the arrival airport of Leg 1 match the departure airport of Leg 2?
- Layover Durations: Is the connection window wide enough for a passenger to clear customs, but tight enough to meet the product’s three-hour restriction?
- Mixed-Cabin Rules: Did the provider stick the passenger in economy on the short feeder leg despite charging them a full business-class fare for the long-haul segment?
The danger here is premature flattening. Explode the array too early without stable parent keys, and your pipeline creates unresolvable segment confetti. Never explode it, and your engine is completely blind to layovers and connection boundaries. The ideal architecture is a hybrid pattern: preserve the full nested payload inside a VARIANT sidecar, explode it temporarily when complex routing logic demands it, and promote the final derived flags into typed columns.
Interrogating Commercial Questions
Once route traversal is solved, we can pivot from solving syntax problems to answering core airline retail questions.
First comes the baseline fare board: finding the absolute lowest price grouped by origin, destination, departure date, and cabin. This requires consistent price extraction and currency normalization across heavily drifted supplier inputs.
From there, the product questions get incredibly specific:
- Baggage-Inclusive Search: The query engine must parse the payload to distinguish included bags from missing data keys, explicit JSON nulls, or paid ancillaries. A lazy null-to-false conversion flags a missing key as “no bag included,” instantly breaking the customer experience and generating support tickets.
- Refundability Audit: Is refundability sent as a clean boolean, a complex penalty tier matrix, or a custom provider extension?
VARIANTallows you to profile these competing shapes side by side before freezing the logic into a typed schema. - Mixed-Cabin Identification: Spotting the premium itineraries where customers accept an economy seat on a short domestic hop specifically to unlock a long-haul business-class suite.
These aren’t abstract syntax exercises. They are travel-commerce mechanics. By querying flexible payloads dynamically, you learn exactly which paths are stable and which ones are volatile, ensuring you only promote fields that have truly earned their schema validation.
🧪 Lab 8: Cheapest Route without Trusting Airport Acronyms
In lab_08__cheapest_route_with_variant.ipynb, we pit legacy JSON string extractions against Spark 4 VARIANT expressions to run an end-to-end itinerary search engine. The lab tests both direct and multi-leg offers, maintains strict parent-offer lineage during array explosions, joins relational airport reference tables, and applies strict baggage, connection, and cabin filters. The goal is to demonstrate exactly how VARIANT simplifies complex hierarchical routing queries, proving that benchmarking metrics only matter after every underlying format returns the exact same commercial truth.
💨 Turbulence: Schema Drift in the Real Cabin
Schema drift is not an exception in commercial data; it’s the weather. Upstream suppliers move fields, rename core business concepts, change data types, wrap scalar strings into heavy nested objects, collapse array elements into singletons, and roll out minor API updates that somehow break three dashboards, two production pipelines, and one data engineer.
VARIANT helps Spark absorb and inspect this turbulence, keeping the aircraft flying while you investigate the storm. But it won’t make modeling decisions for you. You are still the pilot.
Supplier-Specific Fields & Semantic Drift
The first severe turbulence pocket is supplier creativity. One supplier sends fareFamily. Another sends fare_family. A third sends commercial.brand. A fourth sends fareFamilies as an array because apparently one fare can have several competing identities, none of which are emotionally available.
This isn’t just structural drift — it’s semantic drift. The dangerous engineering shortcut is to blindly coalesce every possible JSON path into a single generic column and call it normalization. While this looks tidy on paper, it often quietly misleads the business. In the travel world, a brand might mean a fare family, a localized display label, a specific service bundle, a internal product code, or a pure marketing artifact that should never have boarded a canonical database column.
The healthier architectural pattern is to extract supplier-specific meaning, document the explicit mapping logic, promote a trusted canonical value to Silver, and keep the original supplier branch intact inside the VARIANT sidecar for auditing. When an analyst asks why a fare was classified as Economy Flex, you can show them both the normalized answer and the messy raw evidence. VARIANT doesn’t solve semantic drift; it simply keeps the receipts.
Versioned Payloads
The second pocket of turbulence is time. Even a single supplier will mutate across API versions. Version 1 sends loyaltyTier as a flat string. Version 2 moves it under passenger.loyalty.tier. Version 3 introduces localized benefits. Version 4 transforms everything into a complex object containing the loyalty program name, tier level, member hash, corporate eligibility rules, and a deeply nested reason why lounge access is no longer your problem.
If your pipeline assumes loyalty data is always a simple scalar string, that “minor enhancement” becomes the data equivalent of a heavy beverage cart hitting your knee at cruising altitude.
VARIANT safely absorbs these new hierarchical branches without requiring immediate schema surgery on your ingestion tables. It allows you to inspect shapes, compare versions side by side, and deliberately decide what deserves promotion. But the version-aware parsing logic still belongs to you.
A serious data platform explicitly stores the supplier ID, payload version, event version, ingestion timestamp, and the normalization logic version alongside the record. When fare classifications mysteriously shift, you can point to the data history rather than admitting your silver table “vibes-based its way through a schema change.”
- Raw Payload tells you exactly what arrived at the gate.
VARIANTColumn shows you exactly how the structure changed.- Promoted Columns show you what the platform actively believed.
- Normalization Version tracks which specific rules were applied.
That is how upstream drift becomes explainable. Not painless — just completely explainable.
Contract Tests: Syntax Is Not Trust
The third pocket of turbulence is false comfort. A payload can parse perfectly on a syntax level and still be entirely wrong. Valid JSON does not mean valid airline data, and a successful VARIANT parse doesn’t mean an offer has earned a seat in a Gold table. A syntactically flawless record can still contain a negative fare, an unknown airport code, disconnected route legs, impossible connection timestamps, mismatched currencies, or baggage conditions that commercially mean “good luck at the counter.”
Your pipeline needs rigorous data contract checks that extend far beyond a basic parser:
- Structural Checks: Confirm that required paths exist, expected branches are present, and nested array sizes fall within reasonable operational limits.
- Type Checks: Verify that prices are numeric, passenger counts are integers, timestamps are fully parseable, and refundability rules aren’t written as a philosophical essay.
- Reference Checks: Validate that airport codes, marketing carriers, currencies, and cabin codes exist in trusted, master relational dictionaries.
- Business Logic Checks: Ensure prices are non-negative, route legs connect chronologically without teleportation, itinerary layovers make sense, and taxes reconcile against the total.
This is exactly where lakehouse layers earn their keep. Bronze preserves the raw string evidence. Bronze-Plus parses and profiles the fluid VARIANT. Silver normalizes and executes strict data contracts. Gold serves trusted, analytical outputs. And Quarantine isolates strange records with explicit error reason codes and enough context for data teams to investigate.
The data platform should never merely ask: Can I parse it? It must ask: Can I trust it, explain it, promote it, and safely serve it? Customer-facing commercial promises are much harder to roll back than a broken Spark DataFrame.
🧪 Lab 9: Schema Drift Storm Simulator
In lab_09__schema_drift_storm_simulator.ipynb, we simulate extreme supplier and version drift to see how strict from_json parsing handles structural failure compared to Spark 4 VARIANT inspection. The lab walks through detecting renamed fields, nested path relocations, and object-to-array shifts. It then implements multi-layer contract tests covering required fields, type assertions, master reference data joins, leg continuity, and financial reconciliation. The lab proves a fundamental truth: while VARIANT helps your ingestion pipeline survive structural drift, production tables still demand version-aware normalization, clear promotion rules, and a robust quarantine strategy.
🧊 Cargo Hold: Storage, Parquet, and Why VARIANT Is Not Just a Fancy String
Welcome to the cargo hold, where luggage becomes bytes, files, scans, projections, statistics, compression, and the small financial sadness of reading too much data from cloud storage. Until now, VARIANT has looked like a logical tool for messy payloads. Now comes the colder question: how does it behave when stored?
Raw Strings versus Native Semi-Structured Values
A raw JSON string is honest, useful, and limited. It preserves exactly what arrived, making it perfect for replay, audit, debugging, and supplier arguments where someone says, “We never sent that,” while absolutely sending that.
But to Spark, it’s just text. Inside may live route legs, passengers, taxes, baggage rules, fare families, refund penalties, and supplier extensions. Until parsed, Spark cannot reason much about the structure. If every dashboard extracts origin, destination, total amount, cabin, and baggage status from raw JSON, the query engine is forced to repeatedly open the same suitcase. That’s not flexibility; that’s a luggage inspection simulator with a heavy cloud bill.
VARIANT changes the relationship: Spark stores JSON-like content as native semi-structured data instead of plain text. The payload stays flexible, but it no longer behaves like an opaque string pretending to be architecture.
Still, VARIANT is not the final serving model. The healthy progression is boring and powerful:
- Bronze keeps raw payloads for evidence.
- Bronze-Plus stores parsed
VARIANTfor discovery. - Silver promotes stable fields.
- Gold serves typed business outputs.
Storage is not only about saving bytes. It’s about making common questions cheap and rare questions still possible.
Parquet, Variant Shredding, and the Suitcase Compartment Problem
Parquet likes projection, column pruning, statistics, and skipping work. A giant semi-structured payload gets in the way. If a query only needs origin, destination, and price.total.amount, we should not open the whole commercial organism: passengers, taxes, ancillaries, refund rules, loyalty conditions, and the supplier debug object that should have died in staging.
This is where Variant Shredding (a cornerstone feature of Apache Spark 4’s implementation, aligned with the open Variant spec) comes into play. Variant shredding keeps the full semi-structured value intact while transparently storing frequently accessed paths as strongly typed, columnar fields within the Parquet file under the hood. Common paths benefit from blistering fast columnar projection and statistics-based row-group skipping, while the full VARIANT fallback remains available for rare fields, supplier extensions, and deep investigations.
In airline terms, the full offer still exists, but frequent fields — origin, destination, departure date, fare family, cabin, total amount, currency, baggage status, supplier ID — become much easier to reach.
Powerful? Yes. Magic? No. Shredding is not a free index for every deeply nested path, and it does not replace thoughtful data modeling. It works best when your read patterns identify fields worth optimizing, and it can add write-side overhead during layout processing. So the question is never: Is VARIANT faster? It is: Faster for which paths, under which workload, with which layout, and at what write cost?
The Promotion Rule
A field should not stay in a VARIANT column just because it was born there.
Promote fields that are heavily used in filters, joins, partitioning, clustering, dashboards, route search, pricing analytics, SLAs, customer-facing outputs, or financial reconciliation. Fields like origin, destination, departure_date, supplier, event_type, cabin, fare_family, airport and carrier codes, currency, passenger type, loyalty tier, total_amount, baggage status, and refundability deserve real types, rigid validation rules, and explicit business definitions.
Keep rare fields, supplier extensions, experimental metadata, new event branches, marketing campaign details, debug objects, and one-market oddities in the VARIANT sidecar until they stabilize or prove useless.
- Keep raw JSON for audit, replay, and debugging. Raw payload is evidence, not furniture.
- The worst architecture stores everything as raw strings forever.
- The second worst architecture stores everything as
VARIANTforever and calls it modernization. - The elite architecture is simple: preserve what arrived, parse what needs exploration, promote what the business depends on, keep flexible what is still changing, and quarantine what cannot be trusted.
That is how the cargo hold becomes architecture instead of a dark place where payloads become someone else’s problem.
🧪 Lab 10: Storage Shootout
In lab_10__storage_shootout.ipynb, we write the exact same airline dataset across four different paradigms: raw JSON strings, typed structs, native VARIANT, and a hybrid promoted typed columns plus VARIANT sidecar layout. We throw common queries (e.g., cheapest route, baggage-included fares, fare-family distribution) alongside rare queries (supplier extensions and sustainability metadata) at the data. The lab explicitly measures file size, execution time, output counts, physical scan behavior, column projection, shuffle metrics, memory spill, and repeated parsing pressure. The goal is not to crown an absolute winner, but to give you the data needed to decide what to promote, what can stay in VARIANT, and what raw payloads must remain as evidence.

The survival card for what happens after takeoff: comparing formats, growing realistic test data, querying routes with VARIANT, surviving schema drift, and landing safely with validation, promotion, and quarantine.
📡 Cockpit Instruments: Benchmarking Without Lying to Yourself
Welcome to the cockpit, where we stop trusting data platform vibes: laptop fan noise, notebook satisfaction, cluster aura, and the highly scientific statement, “it just felt faster.”
Benchmarking semi-structured data is an architectural minefield. Extracting a single top-level field from a small, cached JSON block is fundamentally different from parsing 100 megabytes of nested XML, querying shredded VARIANT paths, exploding chronological route arrays, or scanning rare supplier extensions.
This section does not exist to crown JSON, XML, structs, or VARIANT as a universal performance winner. Anyone uttering that sentence should be promptly escorted from the aircraft.
Instead, we are here to build instrumentation that exposes what actually happened under the hood: parsing compute costs, disk scan sizes, shuffle exchanges, data skew, memory spill, and that classic benchmarking villain — accidentally measuring a completely different workload.
What to Measure: Engineering Facts over Emotional Weather
Wall-clock time matters because human beings notice it, but it is a noisy metric heavily polluted by scheduling delays, cloud metadata handshakes, JIT compilation, and background platform noise. To evaluate format efficiency honestly, you must track deterministic, engineering facts:
- Task Time vs. Wall-Clock: Focus heavily on total task time (the sum of execution time across all executor cores). This isolates true computational effort from cluster provisioning friction.
- Input and Output Footprints: Track bytes read from storage, rows processed, files touched, and rows written. If your
VARIANTconfiguration leverages underlying column shredding to read three specific paths while your legacy configuration re-parses the full raw JSON character blob, they are not executing the same physical job. - Shuffle and Spill Tracking: Aggregating data by routes, carriers, or fare families triggers shuffle exchanges. Exploding multi-leg itineraries multiplies rows mid-flight. Keep a sharp eye on disk spill; spilling data to disk means your memory allocation is already knocking at the door with a tiny suitcase and a terrible attitude.
- Execution Graph Auditing: Drill down into the physical Spark execution plan. Verify whether Spark pruned columns, pushed down filters to the storage layer, leveraged Parquet metadata statistics, or invoked expensive row-by-row parsing loops. The plan reveals exactly what your code became after the poetry ended.
- Correctness Tracking: Performance metrics mean nothing without data quality metrics. Monitor counts of malformed structures,
try_parse_jsonnull mappings, failed casts, and quarantine traffic.
A query that runs blazingly fast because it silently dropped eight percent of your flight offers is not optimized. It is a production incident with excellent posture.
The Spark UI as the Black Box Recorder
The Spark UI is your pipeline’s black box recorder. When an execution crashes or bogs down, the UI preserves the exact telemetry required to reconstruct the computational crime scene.
- The SQL/DataFrame Tab: Your map of the physical execution. Use it to check whether Spark reads promoted columns, opens full payloads, parses raw strings, shuffles heavily, or does exactly the thing you hoped it would not. Look closely at the graphical DAG to spot unexpected operators or missing filter pushdowns.
- The Stages Tab: The window into task distribution and data skew. If a single task takes an order of magnitude longer than its peers, your dataset has a skew problem. Perhaps one major hub airport dominates the data, one aggregator sends massive payload clusters, or a single partition is carrying the entire airline industry in a small backpack.
- The Executors Tab: Tells you whether your cluster is running smoothly or quietly melting. It monitors JVM garbage collection overhead, memory utilization, local disk usage, and executor-side task distribution.
- Structured Streaming Metrics: If you are running an in-flight streaming architecture, track input rates, processing rates, trigger batch durations, state store size, watermark tracking, and late-arrival drops. They quickly reveal whether your stream is comfortably keeping pace or slowly building a queue-shaped anxiety sculpture.
Capture these metrics programmatically through Spark listeners or manifest outputs whenever possible. A professional benchmark report should never depend on an engineer squinting at a browser tab saying, “Looks fine to me.” “Looks fine” is an opinion, not an observability strategy.
The Laws of Repeatability
A benchmark without strict repeatability is just a campfire story told with executor cores. To ensure your instruments aren’t lying to you, enforce these constraints:
- Deterministic Seeds: Lock your data factory to a fixed seed. If one run generates simple, direct economy routes and a subsequent run outputs chaotic, multi-leg business class offers with severe XML namespace drift, you are comparing entirely different airports under entirely different weather.
- Enforced Environment Parity: Lock down your cluster size, executor memory allocations, core counts, shuffle partition targets, Adaptive Query Execution (AQE) flags, storage layer configurations, and runtime engine versions.
- The Warm-up Run: Always execute a throwaway warm-up run before recording metrics. The initial run includes metadata catalog loading, file listing overhead, JIT compiler optimization warming, and the JVM remembering that it is alive.
- Statistical Iteration: Run each unique workload multiple times. Record the distribution, variance, and standard deviation of your runs. Investigate weird outliers or spikes instead of deleting them like an inconvenient boarding pass.
Most importantly, preserve identical business semantics. Never compare a raw JSON query that merely extracts an origin string against a VARIANT query that actively validates leg continuity, checks baggage arrays, converts currencies, and routes malformed payloads to a quarantine path. That is comparing hand luggage to a grand piano at a security checkpoint.
Every implementation must answer the exact same business question using identical filter boundaries, null handling rules, and validation logic. If your output row counts differ by even a single record, stop benchmarking immediately. You don’t have a performance optimization problem; you have a correctness problem. Only after your commercial truth matches across all formats does performance become interesting.
🧨 Emergency Exit Row: Failure Modes and Production Pitfalls
Welcome to the emergency exit row: extra legroom, extra responsibility, and the high-stakes job of making sure your pipeline doesn’t slide down an inflatable chute the first time it meets broken XML, negative tax surcharges, an out-of-order fare update, or a supplier extension named misc_final_FINAL_v2.
Spark 4 hands us exceptionally sharp tools: native VARIANT handling, built-in XML parsing, safe path extractions, structured streaming watermarks, and granular schema options. Sharp tools are incredibly useful, but they will absolutely cut teams who confuse sheer structural flexibility with actual data architecture.
VARIANT Everywhere Is Not Architecture
It is remarkably easy to fall in love with VARIANT. It gracefully accepts evolving JSON payloads, keeps deeply nested data queryable, and grants data engineering teams immense breathing room before they have to commit to a stable schema.
But if every single business-critical field stays buried inside a VARIANT column forever, your data lakehouse mutates into an endless path-extraction festival. Downstream dashboards will repeatedly duplicate extraction logic, core joins will rely on brittle string keys, analysts will invent five competing definitions of what constitutes an included bag, and data governance becomes an exercise in interpretive dance.
VARIANT must be treated as a flexible staging, profiling, and inspection layer—not the entire airport. Fields driving search, filtering, partition layouts, core joins, financial reconciliation, or customer-facing pricing APIs must earn a promoted, typed seat. The VIP lounge is not the terminal.
Strict Schemas Everywhere Are Not Architecture Either
The opposite engineering mistake is overcorrecting into a tyrant: forcing every piece of incoming operational data to salute one rigid, monolithic mega-schema. This feels highly responsible until a third-party partner rolls out an optional API branch, mutates a scalar string into an object, or appends a brand-new promotional benefit your schema has never met. Suddenly, the entire pipeline fails loudly, nulls out massive chunks of valid data, or silently discards critical payload fields you’ll desperately need next week.
Strict, unyielding schemas are phenomenal for your trusted Silver and Gold consumption tables. They are disastrous as the only gatekeeper standing at the messy point of ingestion.
The resilient pattern is intentionally multi-layered:
- Bronze: Raw string payloads preserve the original, unadulterated evidence.
- Bronze-Plus: Parsed
VARIANTcolumns facilitate structural fluidity and safe discovery. - Silver: Strongly typed columns promote and validate stable business meaning.
- Quarantine: Isolates payloads that are syntactically broken or commercially unsafe.
Accept structural flexibility at the gate. Enforce strict mathematical rigor in the boardroom.
XML Gotchas: Guarding the Tree
XML is a tree-shaped format carrying enough historical design baggage to make your data carousel file a formal complaint. When using Spark 4’s native XML engine, beware of these three core pitfalls:
- The
rowTagIllusion: This parameter defines which repeating XML element is chopped into a DataFrame row. Configure it incorrectly, and Spark will still cheerfully run your code, but it will output a fundamentally warped analytical shape. Producing confident, well-formatted nonsense is far worse than a loud runtime crash. - The Attribute & Namespace Sabotage: Enterprise systems pack critical business metadata — such as currency hints, airport codes, tax identifiers, or loyalty levels — directly into XML attributes and distinct namespaces. Stripping or ignoring them because they are annoying to parse is quiet data destruction disguised as a pipeline cleanup.
- The Permissive Trap: Choosing how to parse bad XML is a business risk calculation. If you need a ironclad contract,
FAILFASTis mandatory. If you are processing massive ingestion streams, usePERMISSIVEparsing combined with a robust quarantine path. But never merely drop malformed records or route them to a silent black hole; that is evidence destruction with better corporate branding.
JSON Gotchas: Navigating the Nesting Dolls
JSON feels friendlier than XML, but it is friendly in the exact same way airport Wi-Fi is friendly: mostly fine until the precise moment you rely on it.
Watch out for numbers arriving as text strings, booleans sent as regional characters, and arrays that morph into objects the moment a supplier sends a single item instead of a list. The sneakiest performance killer is massive nested array explosion. Exploding passengers, legs, taxes, baggage rules, and corporate discounts in a single unoptimized query multiplies rows exponentially. A harmless-looking flight offer can rapidly balloon into a small distributed weather system that overwhelms your executors.
Repeated string parsing via get_json_object is a lazy anti-pattern, not a data model. If every subsequent query is forcing Spark to poke raw text strings to locate an origin, destination, or total price, you aren’t being flexible—you are paying a massive, recurring computational parsing tax. Parse intentionally, extract safely using try_variant_get, validate early, and promote aggressively. Valid JSON syntax never equates to valid business data.
Streaming Gotchas: Micro-Batches & Infinite Anxiety
Streaming semi-structured data turns batch-level headaches into a recurring, localized storm system. Broken formats, upstream supplier drift, duplicate events, late-arriving corrections, and expired prices don’t hit your pipeline once — they flood it continuously.
- Watermarks 🆚 Business TTLs: A Spark Structured Streaming watermark is purely an engine mechanic designed to manage event-time lateness and clean up state-store memory. An airline fare Time-To-Live (TTL) is a strict commercial constraint defining how long a shopping price is valid for purchase. Mixing these two concepts up is how expired, stale prices get served to customers because the streaming engine was technically on time.
- The Unbounded State Nightmare: If your stateful streaming operations group data too broadly, cache full nested
VARIANTstructures, forget to implement strict eviction metrics, or fail to drop duplicates, your state store will expand exponentially until the cluster writes its final goodbye letter to disk. - The Micro-Batch Parsing Tax: Re-parsing massive raw string payloads inside every single sub-second micro-batch works fine during low-volume staging, but it quickly becomes a production performance villain under peak load.
To build a flawless streaming architecture, extract stable routing and grouping keys early in the lifecycle. Keep raw payloads for auditing, leverage VARIANT sidecars for elastic tracking, and never shove an entire unparsed payload into your streaming state store. A production streaming pipeline must know exactly what is late, what is duplicated, what is malformed, what is commercially expired, and what is safe to serve. Anything less is just turbulence with logs.
🛬 Landing: Final Thoughts
Spark 4 doesn’t eliminate semi-structured chaos. It gives chaos better boarding documents.
VARIANTmakes evolving JSON-like payloads easier to inspect. Native XML support removes a long-standing integration headache. Pipelines become safer when raw preservation, flexible parsing, typed promotion, and quarantine work together instead of arguing at the gate.
But the lesson is not: Use VARIANT. The lesson is architecture.
Keep the raw payload. Parse what you can. Promote what you trust. Quarantine what lies. Measure what hurts. And never let a business dashboard depend on twenty-seven fragile path extractions because nobody designed the silver table.
The aircraft lands safely when we stop asking, “JSON, XML, or VARIANT?”
And start asking: What belongs in bronze, bronze-plus, silver, gold, and the overhead bin?
Welcome to Spark 4. Please remain seated until schema drift has come to a complete stop.
🫡 Crew Acknowledgements
Before landing, a quick salute to my flight crew: David, Marina, Pedro, Toni, and our dashboard-man Sergio.
Thanks for keeping the aircraft airborne, the pipelines only mildly haunted, and the dashboards just suspicious enough to be useful.
“Once Spark lifts us up there, where the bytes are stringified
We’ll just glide, schema-eyed
Once Spark lifts us up there, with VARIANT very near
You might even hear the whole cluster cheer
Just because we parse together”
(adapted from “Come fly with me” by Frank Sinatra)

Spark 4 flight simulator mode: measure facts, not vibes; keep VARIANT out of the cockpit ego contest; and please remain seated until schema drift has come to a complete stop.
