CertSafari
    Databricks Certified Data Engineer Professional· Lessons

    Domain 3 · Lesson 19/46

    Modeling Semi-Structured Data with the VARIANT Type

    Develop a model and query semi-structured data using the VARIANT data type and related functions (e.g., parse_json, variant_get).

    8 min read
    3% of exam
    6 sources
    Published 2 Oct 2026
    Docs as of 30 Sep 2026

    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.

    The four documented approaches for storing semi-structured data
    ApproachBest forSchema handlingPerformance
    VariantSemi-structured JSON that benefits from optimized storageFlexible, with similar applications to JSON stringsOptimized encoding that outperforms JSON strings on read and write
    JSON stringsCapturing a complete, accurate representation of raw source dataNo type information and no pre-processing requiredWorst on read, because the entire string is parsed for every query
    StructsData with well-known schemas where you want validated dataEnforced on write; requires pre-processing; less flexible for schema evolutionBest on read, but degrades past hundreds of columns
    Maps and arraysSemi-structured datasets with around 500 fieldsKeys and values are typed and enforced on writeBalanced 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?

    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:

    CTAS that promotes id to its own column and keeps the full record as VARIANTsql
    CREATE TABLE table_name AS
      SELECT json_string:id AS id,
        PARSE_JSON(json_string) variant_column
      FROM source_data

    Pulling 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.

    Appending JSON strings into an existing VARIANT columnsql
    INSERT INTO table_name (variant_column)
      SELECT PARSE_JSON(json_string)
      FROM source_data

    Checkpoint 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?

    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?

    Sources23

    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.

    parse_json on an object, on a scalar, and on invalid inputsql
    -- 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_PARSING

    When 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}');

    to_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.

    Sources45

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 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. 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. 1.
      “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. 2.
      “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. 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. 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. 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

    Continue to page 2 of 2

    Querying VARIANT Data with variant_get, Path Syntax, and Variant Functions

    Spotted a mistake, or was something unclear? Tell us.