What you will be able to do
- Explain how VARIANT, OBJECT and ARRAY nest to hold semi-structured data
- Convert JSON text to a queryable VARIANT with PARSE_JSON and predict its NULL and duplicate-key behaviour
- Traverse JSON with colon, dot and bracket notation, casting results to SQL types
- Expand nested arrays into rows with LATERAL FLATTEN and choose the right FLATTEN output column
Key concept
VARIANT as the container for semi-structured data — Snowflake stores hierarchical data such as JSON in a VARIANT, which can hold any value, including OBJECTs and ARRAYs whose elements are themselves VARIANTs. You query it with path operators and FLATTEN instead of defining a fixed schema first.
1.Why semi-structured data lands in a VARIANT
A relational table needs its schema defined before any data arrives. JSON doesn't work that way. New attributes can appear at any time, two records of the same kind can carry different keys, and values nest to any depth. The Snowflake documentation describes semi-structured data as data that "does not require a prior definition of a schema and can constantly evolve".
Snowflake handles this with three data types that nest inside each other. An OBJECT is a set of key-value pairs, which the docs compare to a "dictionary" or "map". An ARRAY is an ordered list. A VARIANT can hold either of these, or any scalar. The two containers always hold VARIANTs, so a hierarchy is built like this: an OBJECT whose value is a VARIANT that wraps an ARRAY, whose cells are VARIANTs that wrap OBJECTs, and so on as deep as the data goes.
You don't usually build this structure by hand. When you load a format Snowflake recognises and parses (JSON, Avro, Parquet or ORC), the data is converted into this internal representation automatically. You can store a whole document in one VARIANT column, or split chosen parts into separate typed columns. The usual pattern sets the column type to VARIANT and declares the input format in the file format of the COPY command:
CREATE TABLE my_table (my_variant_column VARIANT); COPY INTO my_table ... FILE FORMAT = (TYPE = 'JSON') ...Checkpoint 1 of 7· Check yourself
A JSON document has a top-level array, and each cell holds an object with a nested array of readings. After you load it with TYPE = 'JSON', what type does Snowflake use for each element inside an ARRAY or OBJECT?
The containers always hold VARIANTs. This is what lets a hierarchy nest to any depth without a predefined schema.
“An ARRAY or OBJECT holds a value of type VARIANT.”Source: docs.snowflake.com
Sources1
2.Turning JSON text into a VARIANT with PARSE_JSON
JSON often arrives as plain text: a VARCHAR column, a string from an API, or a value in a VALUES list. Path notation can't reach inside a string, so first you convert it. PARSE_JSON "Interprets an input string as a JSON document, producing a VARIANT value." TO_JSON goes the other way, from VARIANT back to a string. The two are only almost reciprocal, because TO_JSON doesn't preserve key order or whitespace.
Two other edge cases come up in exam questions. First, SQL NULL and JSON null are different things. PARSE_JSON(NULL) returns SQL NULL, but PARSE_JSON('null') returns a valid VARIANT that contains a JSON null. Second, numbers are typed as they're parsed. A plain decimal keeps exact precision, "treating 123.45 as NUMBER(5,2), not as a DOUBLE value", while scientific notation becomes DOUBLE. JSON has no native date or timestamp, so those values stay strings until you convert them. The table below shows the TYPEOF results from the PARSE_JSON reference example.
| Input string | TYPEOF result |
|---|---|
| 'null' | NULL_VALUE (a JSON null inside a VARIANT) |
| SQL null | NULL (SQL NULL) |
| '123.12' | DECIMAL |
| '1.912e2' | DOUBLE |
| '[-1, 12, 289, 2188, false,]' | ARRAY |
| '{ "x" : "abc", "y" : false, "z": 10} ' | OBJECT |
Duplicate keys are the last thing to watch for. By default PARSE_JSON is strict and rejects an object that repeats a key, with an error such as "duplicate object attribute". If you pass the optional 'd' parameter, the duplicates are accepted and only the last value for each repeated key is kept.
Checkpoint 2 of 7· Fill the gap
This insert parses an object that repeats key "a". Which parameter makes it succeed and keep "a": "789"?
INSERT INTO vartab
SELECT column1 AS n, PARSE_JSON(column2, ' ? ') AS v
FROM VALUES (10, '{ "a" : "123", "b" : "456", "a": "789"} ')
AS vals;'d' allows duplicate keys and keeps the last value. 's' is the strict default, which raises an error.
Source: docs.snowflake.comCheckpoint 3 of 7· Check yourself
A staging column holds the literal string 'null' in some rows. After PARSE_JSON, which statement is true for those rows?
Only a SQL NULL input gives SQL NULL. The string 'null' is valid JSON, and TYPEOF reports it as NULL_VALUE.
“if the input string is 'null', then it is interpreted as a JSON null value”Source: docs.snowflake.com
Sources2
3.Traversing JSON with colon, dot and bracket notation
Once the data is in a VARIANT, you navigate it with path operators. A colon joins the column to the first-level key, as in src:dealership. From there you go deeper with dot notation (src:salesperson.name) or bracket notation with single-quoted names (src['salesperson']['name']). The two notations return the same result.
SELECT src:salesperson.name
FROM car_sales
ORDER BY 1;SELECT src['salesperson']['name']
FROM car_sales
ORDER BY 1;There are three rules to remember. First, case: the column name is case-insensitive but element names are case-sensitive, so SRC:salesperson.name works and SRC:Salesperson.Name does not. Second, quoting: a key that isn't a valid SQL identifier, such as one containing a space or starting with a digit, must be wrapped in double quotes, as in src:"company name". Third, output type: the result is still a VARIANT, which is why the docs show values like "Frank Beasley" in double quotes. To get a plain SQL value for a consumable table, cast it, for example value:name::string.
Checkpoint 4 of 7· Check yourself
Which of these paths does NOT return the salesperson's name from the car_sales data?
The column name's case doesn't matter, but the JSON keys are lowercase, and element names are matched case-sensitively, so Salesperson and Name don't match.
“the column name is case-insensitive but element names are case-sensitive”Source: docs.snowflake.com
Checkpoint 5 of 7· Exam question
An `orders` table has columns `order_id NUMBER` and `payload VARIANT`. Each payload looks like `{"customer":"Ana","items":[{"sku":"A1","qty":2},{"sku":"B7","qty":1}]}`. An analyst needs one output row per purchased item with `order_id`, `sku`, and `qty`. Which query does this?
Correct answer: B — SELECT o.order_id, i.value:sku::STRING AS sku, i.value:qty::NUMBER AS qty FROM orders o, LATERAL FLATTEN(input => o.payload:items) i
- A. Restricting FLATTEN to the path 'sku' looks for a key named sku at the top of the document, where none exists, so no rows come back. The array of items is never expanded, which makes this unusable.
- B. Correct. LATERAL FLATTEN on the items array emits one row per element, and the VALUE column holds each item object so sku and qty can be extracted and cast. Each output row stays correlated with its parent order_id.
- C. This still reads sku and qty from the whole items array instead of from the flattened element. Path notation cannot reach into an array without an index, so the extracted columns come back NULL.
- D. Flattening the whole payload yields one row per top-level key, namely customer and items. The VALUE column then holds a string or the array, so sku and qty resolve to NULL.
Sources3
4.Expanding arrays into rows with LATERAL FLATTEN
Path notation gives you one value per row. When a key holds an array, like the customer or vehicle arrays in car_sales, you usually want one row per element. That's what FLATTEN does. The docs call it "a table function that produces a lateral view of a VARIANT, OBJECT, or ARRAY column". With the LATERAL modifier, each output row is joined back to the source row it came from, so columns from the original table repeat once for each element.
SELECT
value:name::string as "Customer Name",
value:address::string as "Address"
FROM
car_sales
, LATERAL FLATTEN(INPUT => SRC:customer);FLATTEN always returns the same fixed set of columns. The query above reads VALUE and then applies path notation to it. For a single-level array, TABLE(FLATTEN(...)) and LATERAL FLATTEN(...) return the same result. For nested arrays, such as extras inside vehicle, you chain FLATTEN calls, and LATERAL is what lets each call reference the output of the one before. The MODE argument limits expansion to 'OBJECT', 'ARRAY' or 'BOTH' (the default). A recursive setting, off by default, expands every sub-element instead of only the one at the given path.
| Column | What it contains |
|---|---|
| SEQ | A unique sequence number for the input record (not guaranteed gap-free or ordered) |
| KEY | For objects or maps, the key of the exploded value |
| PATH | The path to the element within the structure |
| INDEX | The element's position if it is an array; otherwise NULL |
| VALUE | The element of the flattened array or object |
| THIS | The element being flattened (useful in recursive flattening) |
Checkpoint 6 of 7· Match them up
Match each FLATTEN output column to what it returns
Tap a term, then the definition that fits it.
INDEX is populated only for arrays and KEY only for objects. VALUE holds the element itself, and THIS holds its container.
“The index of the element, if it is an array; otherwise NULL.”Source: docs.snowflake.com
Checkpoint 7 of 7· Exam question
A `raw_events` table stores JSON in a `VARIANT` column named `raw`. Documents are stored as `{"UserId":42,"Country":"NL"}`. A dashboard query `SELECT raw:userid::NUMBER, raw:country::STRING FROM raw_events` returns NULL for every row, although the data is present. What explains this and fixes it?
Correct answer: C — Key names in path notation are case-sensitive, so the query must use the stored spelling and read `raw:UserId` and `raw:Country` with the casts
- A. Snowflake keeps the key casing exactly as it was in the JSON document and does not lowercase anything at load time. Quoting a lowercase key would still fail to match the stored UserId key.
- B. That parameter affects how quoted SQL identifiers such as column names are resolved. It has no effect on key names inside a VARIANT, which are always matched case-sensitively.
- C. Correct. Element names after the colon are matched case-sensitively against the document, so userid and country find nothing and return NULL. Using the exact casing UserId and Country returns the values.
- D. VARIANT values can be cast directly with the double-colon operator, which is the standard way to type extracted elements. The NULLs come from the key mismatch, not from the casts.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.JSON paths are case-insensitive like the rest of Snowflake SQL, so src:Vehicle and src:vehicle are interchangeable.Why is that wrong?
Only the column name is case-insensitive. JSON element names must match the key's case exactly.
Covered in Traversing JSON with colon, dot and bracket notation
2.PARSE_JSON quietly keeps the last value when an object repeats a key.Why is that wrong?
Strict mode is the default, and repeated keys raise an error. Only the 'd' parameter makes PARSE_JSON keep a single instance with the last value.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Semi-structured data does not require a prior definition of a schema and can constantly evolve”
↩︎ Why semi-structured data lands in a VARIANT“the data is converted to an internal data format that uses Snowflake VARIANT, ARRAY, and OBJECT data types.”
↩︎ Why semi-structured data lands in a VARIANT“A VARIANT can hold a value of any other data type, including an ARRAY or an OBJECT.”
↩︎ Key concept“An ARRAY or OBJECT holds a value of type VARIANT.”
↩︎ Checkpoint - 2.
“Interprets an input string as a JSON document, producing a VARIANT value.”
↩︎ Turning JSON text into a VARIANT with PARSE_JSON“treating 123.45 as NUMBER(5,2), not as a DOUBLE value”
↩︎ Turning JSON text into a VARIANT with PARSE_JSON“the returned object has a single instance of that key with the last value specified for that key.”
↩︎ Turning JSON text into a VARIANT with PARSE_JSON“By default, the function doesn’t allow duplicate keys in the JSON object”
↩︎ Exam trap 2“then the function returns NULL (rather than raising an error)”
↩︎ Prediction“if the input string is 'null', then it is interpreted as a JSON null value”
↩︎ Checkpoint - 3.
“Insert a colon : between the VARIANT column name and any first-level element”
↩︎ Traversing JSON with colon, dot and bracket notation“Operators : and subsequent . and [] always return VARIANT values containing strings.”
↩︎ Traversing JSON with colon, dot and bracket notation“then you must enclose the name in double quotes”
↩︎ Traversing JSON with colon, dot and bracket notation“FLATTEN is a table function that produces a lateral view of a VARIANT, OBJECT, or ARRAY column.”
↩︎ Expanding arrays into rows with LATERAL FLATTEN“the column name is case-insensitive but element names are case-sensitive”
↩︎ Exam trap 1“the column name is case-insensitive but element names are case-sensitive”
↩︎ Checkpoint - 4.
“use LATERAL so that each subsequent FLATTEN can reference the output of the previous one.”
↩︎ Expanding arrays into rows with LATERAL FLATTEN“the values in this input row are replicated to match the number of rows produced by FLATTEN.”
↩︎ Expanding arrays into rows with LATERAL FLATTEN“The index of the element, if it is an array; otherwise NULL.”
↩︎ Checkpoint