What you will be able to do
- Choose between VARIANT, JSON strings, structs, and maps/arrays for a semi-structured workload
- Create tables with VARIANT columns using CREATE TABLE, CTAS with parse_json, and INSERT
- Design around VARIANT limitations by promoting frequently filtered fields to typed columns
- Choose between parse_json and try_parse_json for malformed input, and explain why to_json is not an exact inverse
Key concept
VARIANT data type — VARIANT is a Databricks SQL type that stores semi-structured data (objects, arrays, and scalars) in an optimized binary encoding. It keeps a flexible schema like a JSON string does, but reads and writes faster, so it is the recommended default for semi-structured data.
1.Choosing a storage pattern for semi-structured data
Before you write a query you have to decide how the data is stored. Databricks describes four patterns for semi-structured data. Three questions decide between them: how often the fields or types in the source change, how many unique fields there are, and whether the workload is optimized for reads or for writes. Whichever pattern you pick, the data should still live in Delta tables.
| Approach | Best for | Schema handling | Performance |
|---|---|---|---|
| Variant | Semi-structured JSON that benefits from optimized storage | Flexible, with similar applications to JSON strings | Optimized encoding that outperforms JSON strings on read and write |
| JSON strings | Capturing a complete, accurate representation of raw source data | No type information and no pre-processing required | Worst on read, because the entire string is parsed for every query |
| Structs | Data with well-known schemas where you want validated data | Enforced on write; requires pre-processing; less flexible for schema evolution | Best on read, but degrades past hundreds of columns |
| Maps and arrays | Semi-structured datasets with around 500 fields | Keys and values are typed and enforced on write | Balanced on read and write, but statistics can't be collected |
The table shows the trade-off. A JSON string has no type information, so every query has to re-parse the whole string. A struct gives the fastest reads, but its schema is enforced on write, and it copes poorly with evolving types or new fields. VARIANT sits between them: its schema is as flexible as a JSON string's, but its encoding beats JSON strings on both read and write. Databricks says new users should choose VARIANT whenever the schema is changing or unknown. For data with a well-known schema, structs, maps, and arrays still have a place, because they allow optimized data layout and statistics collection.
Checkpoint 1 of 4· Check yourself
A team ingests clickstream events from many app versions. New fields appear every few weeks and the full set of fields is not known up front. They currently store each event as a JSON string. What does Databricks recommend?
VARIANT is recommended for semi-structured data whose schema is changing or unknown. It keeps the flexibility of JSON strings and performs better on read and write.
“you should use variant over JSON strings whenever storing semi-structured data that requires flexibility for changing or unknown schema”Source: docs.databricks.com
Sources1
2.Creating VARIANT columns and promoting hot fields
VARIANT is a standard SQL type, and Delta Lake tables support it. You can declare an empty column directly with CREATE TABLE table_name (variant_column VARIANT). More often you convert existing JSON text with parse_json inside a CTAS. The documented example also pulls one field out into its own column in the same statement:
CREATE TABLE table_name AS
SELECT json_string:id AS id,
PARSE_JSON(json_string) variant_column
FROM source_dataPulling id out is a deliberate modeling choice, not a convenience. Databricks recommends storing frequently queried fields as separate non-variant columns, to speed up queries and optimize storage layout. It also has to work this way because of hard limits on the type: a VARIANT column cannot be a clustering key, a partition column, or a Z-order key. VARIANT values also cannot be used in comparisons, grouping, ordering, or set operations. So any field you need to cluster on or group by has to exist as a typed column next to the variant.
When the target table already has a VARIANT column, you load new records the same way, using INSERT ... SELECT PARSE_JSON(...). The other routes into the type are CAST, which converts a literal to VARIANT, and to_variant_object, which converts a STRUCT or MAP value (MAP keys must be STRING). Two further constraints affect table design. A single VARIANT record can be at most 16 MB. Iceberg v2 tables do not support VARIANT columns; Iceberg v3 does.
INSERT INTO table_name (variant_column)
SELECT PARSE_JSON(json_string)
FROM source_dataCheckpoint 2 of 4· Check yourself
An events table stores each full event in a VARIANT column named payload. Analysts filter on event_type in almost every query, and the team wants to use liquid clustering on it. What is the right model?
VARIANT columns cannot be clustering, partition, or Z-order keys. Databricks recommends storing frequently queried fields as separate non-variant columns, and that column is the one you cluster on.
“VARIANT columns cannot be used for clustering keys, partitions, or Z-order keys.”Source: docs.databricks.com
Checkpoint 3 of 4· Exam question
A data engineer is building a bronze Lakeflow Spark Declarative Pipeline that ingests raw JSON event payloads from cloud storage via Auto Loader. The payloads have a frequently changing schema, and downstream consumers need to query arbitrary nested fields without the engineer having to pre-define or evolve a STRUCT schema for every new field. Which approach correctly loads the raw JSON string column into a queryable semi-structured representation in the bronze table?
Correct answer: A — Convert the raw JSON string with `parse_json()` before writing to bronze, storing it as a `VARIANT` column that supports `variant_get` and `:` path access without a predefined schema.
- A. Wrapping the raw string with `parse_json()` produces a `VARIANT` value that Databricks recommends for semi-structured data precisely because it preserves the full nested structure without requiring a fixed schema up front. Fields can then be pulled out on demand with `variant_get` or the `:` shorthand as consumers need them, which matches the flexibility requirement.
- B. An explicit STRUCT schema is the opposite of schema flexibility: every new field forces a schema evolution event, and Auto Loader's evolution modes typically fail or restart the stream rather than silently rewriting the STRUCT mid-flight. This adds operational overhead the scenario is trying to avoid.
- C. Leaving the payload as a STRING and re-parsing it with `get_json_object()` on every query works but throws away the structural parsing benefits of `VARIANT`, forcing repeated string-based JSON parsing at query time instead of a single upfront conversion.
- D. Automatic flattening into STRUCT columns commits the table to a rigid, field-by-field schema derived from whatever sample Auto Loader inferred, which breaks or requires evolution the moment a genuinely new or inconsistent field shows up in later files.
3.parse_json, try_parse_json, and the round trip to JSON text
parse_json(jsonStr) takes a STRING containing a JSON document and returns the equivalent VARIANT value. The document does not have to be an object: a bare scalar such as '123' parses fine. parse_json is strict, though. It raises an error when the string is malformed, invalid, or too large for a variant. In a pipeline, one bad record would then fail the whole query.
-- Simple example
> SELECT parse_json('{"key": 123, "data": [4, 5, "str"]}');
{"data":[4,5,"str"],"key":123}
-- Parsing a scalar value
> SELECT parse_json('123');
123
-- invalid JSON string
> SELECT parse_json('{ bad }');
Error: MALFORMED_RECORD_IN_PARSINGWhen you would rather keep the row and handle the failure later, use try_parse_json. It has the same purpose, but it returns NULL when parsing fails. Both functions exist in SQL and in Python (pyspark.sql.functions).
Checkpoint 4 of 4· Fill the gap
This query must return NULL for the malformed document instead of failing. Which function fills the blank?
SELECT ? ('{"a" : invalid, "b" : 2}');try_parse_json returns NULL when parsing fails. parse_json would raise an error on this input.
Source: docs.databricks.comto_json goes the other way and turns a VARIANT back into a STRING. It is the logical inverse of parse_json, but not an exact one. Whitespace is not preserved, key order is arbitrary (the example above came back with data before key), and trailing zeros in numbers may be truncated. A test that compares raw JSON text with to_json(parse_json(...)) can therefore fail even when the data is identical.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.You can partition, cluster, or Z-order a table by its VARIANT column, or GROUP BY / ORDER BY a VARIANT value.Why is that wrong?
VARIANT cannot be a clustering, partition, or Z-order key, and it cannot be used in comparisons, grouping, ordering, or set operations. Extract the field you need into a typed column.
Covered in Creating VARIANT columns and promoting hot fields
2.to_json(parse_json(s)) always gives back exactly the original string s.Why is that wrong?
to_json is only the logical inverse of parse_json. Whitespace, key order, and trailing zeros in numbers can all change.
Covered in parse_json, try_parse_json, and the round trip to JSON text
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.https://docs.databricks.com/aws/en/semi-structuredOfficial docs
“Optimized encoding that outperforms JSON strings on read and write”
↩︎ Choosing a storage pattern for semi-structured data“Some workloads still benefit from using structs, maps, and arrays, especially for data with well-known schemas”
↩︎ Choosing a storage pattern for semi-structured data“Best on read, but degrades past hundreds of columns”
↩︎ Prediction - 2.https://docs.databricks.com/aws/en/ingestion/variantOfficial docs
“Databricks recommends extracting frequently queried fields and storing them as non-variant columns to accelerate queries and optimize storage layout.”
↩︎ Creating VARIANT columns and promoting hot fields“VARIANT type can only hold records up to 16 MB in size.”
↩︎ Creating VARIANT columns and promoting hot fields“VARIANT columns cannot be used for clustering keys, partitions, or Z-order keys.”
↩︎ Checkpoint - 3.
“To store STRUCT and MAP use the to_variant_object function. MAP keys must be of type STRING.”
↩︎ Creating VARIANT columns and promoting hot fields“Iceberg v2 tables do not support VARIANT columns. Apache Iceberg v3 supports VARIANT columns.”
↩︎ Creating VARIANT columns and promoting hot fields - 4.
“The parse_json function returns an error if the JSON string is malformed, exceeds the variant size limit, or is invalid.”
↩︎ parse_json, try_parse_json, and the round trip to JSON text“Trailing zeros in numbers might be truncated.”
↩︎ parse_json, try_parse_json, and the round trip to JSON text“you should use variant over JSON strings whenever storing semi-structured data that requires flexibility for changing or unknown schema”
↩︎ Checkpoint - 5.
“To return NULL instead of an error, use the try_parse_json function.”
↩︎ parse_json, try_parse_json, and the round trip to JSON text“it is not an exact inverse, so to_json(parse_json(jsonStr)) = jsonStr may not be true.”
↩︎ Exam trap 2“If the JSON string is not valid, Databricks raises MALFORMED_RECORD_IN_PARSING.”
↩︎ Prediction
Also cited
“Databricks recommends using VARIANT over JSON strings for semi-structured data.”
↩︎ Key concept“The VARIANT data type cannot be used for comparisons, grouping, ordering, and set operations.”
↩︎ Exam trap 1