Home Database Semi-Structured Data: JSON, Parquet, Avro & VARIANT
Intermediate 5 min · July 17, 2026

Semi-Structured Data: JSON, Parquet, Avro & VARIANT

Master Snowflake's VARIANT data type to query JSON, Parquet, Avro, and other semi-structured formats.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 17, 2026
last updated
2,439
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic SQL knowledge (SELECT, WHERE, JOINs)
  • Familiarity with JSON structure (objects, arrays, key-value pairs)
  • Snowflake account with access to create tables and stages
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Snowflake uses the VARIANT data type to store semi-structured data like JSON, Avro, Parquet, and XML. You can query nested data using dot notation, bracket notation, FLATTEN, and LATERAL FLATTEN. Key functions include PARSE_JSON, TRY_PARSE_JSON, GET, and ARRAY_SIZE. Performance tips: use explicit casting, avoid deeply nested structures, and leverage materialized views for repeated queries.

✦ Definition~90s read
What is Semi-Structured Data?

Snowflake's VARIANT data type lets you store and query semi-structured data like JSON, Parquet, and Avro directly using SQL, without needing a predefined schema.

Think of Snowflake's VARIANT like a magic box that can hold any kind of messy data—like a drawer where you throw receipts, sticky notes, and photos.
Plain-English First

Think of Snowflake's VARIANT like a magic box that can hold any kind of messy data—like a drawer where you throw receipts, sticky notes, and photos. Normally, you'd need separate labeled folders (columns) for each item, but VARIANT lets you keep everything in one flexible container. Then, you can use special tools (functions) to reach in and pull out exactly what you need, even if the contents are different shapes and sizes.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

In the modern data landscape, semi-structured data—JSON logs, Parquet files from data lakes, Avro messages from Kafka—is everywhere. Traditional relational databases struggle with this data because they require a rigid schema upfront. Snowflake solves this with the VARIANT data type, a flexible column that can store any semi-structured format and query it using SQL extensions.

Imagine you're a data engineer at an e-commerce company. Your application sends user clickstream events as JSON objects with varying fields: sometimes a 'product_id', sometimes a 'search_query', and occasionally an 'error_code'. With Snowflake's VARIANT, you can ingest all these events into a single table without preprocessing, then use SQL to extract and analyze the relevant fields on the fly.

In this tutorial, you'll learn how to define VARIANT columns, load JSON/Parquet/Avro data, query nested structures using dot and bracket notation, flatten arrays with FLATTEN, and optimize performance. We'll also cover a real production incident where a missing NULL check caused a data pipeline to fail, and provide a debugging guide for common issues. By the end, you'll be able to handle semi-structured data in Snowflake with confidence.

What is Semi-Structured Data and VARIANT?

Semi-structured data doesn't fit neatly into rows and columns but has tags or markers to separate data elements. Common formats include JSON, Avro, Parquet, ORC, and XML. Snowflake's VARIANT is a universal data type that can hold any of these formats. Internally, Snowflake stores VARIANT data in a compressed, columnar format optimized for query performance.

Key characteristics
  • VARIANT can store objects, arrays, numbers, strings, booleans, and NULLs.
  • You can mix different structures within the same column (schema-on-read).
  • Snowflake automatically detects the format when loading (e.g., JSON vs Avro).

To define a VARIANT column: ``sql CREATE TABLE events ( event_id INTEGER AUTOINCREMENT, event_data VARIANT, ingested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP() ); ``

You can also use OBJECT and ARRAY subtypes for stricter typing, but VARIANT is the most flexible.

create_variant_table.sqlSQL
1
2
3
4
5
CREATE TABLE events (
    event_id INTEGER AUTOINCREMENT,
    event_data VARIANT,
    ingested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
);
Output
Table EVENTS successfully created.
🔥VARIANT vs OBJECT vs ARRAY
📊 Production Insight
In production, avoid storing extremely large VARIANT values (> 10 MB) as they can slow down queries. Consider splitting large blobs into separate tables.
🎯 Key Takeaway
VARIANT is Snowflake's flexible column type for semi-structured data. Use it to store JSON, Avro, Parquet, and more without predefined schemas.

Loading Semi-Structured Data into Snowflake

Snowflake supports loading semi-structured data from internal or external stages using COPY INTO. The file format must be specified (JSON, Avro, Parquet, ORC, XML).

Example: Loading JSON files from an S3 stage.

First, create a file format: ``sql CREATE FILE FORMAT my_json_format TYPE = JSON COMPRESSION = AUTO STRIP_OUTER_ARRAY = TRUE; ``

Then, copy data: ``sql COPY INTO events (event_data) FROM @my_s3_stage FILE_FORMAT = (FORMAT_NAME = my_json_format) ON_ERROR = 'SKIP_FILE'; ``

For Avro and Parquet, Snowflake automatically infers the schema and maps fields to VARIANT. You can also use VALIDATION_MODE to check for errors before loading.

Tip: Use STRIP_OUTER_ARRAY = TRUE for JSON files that contain an array of objects; it will load each object as a separate row.

load_json.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
-- Create file format
CREATE FILE FORMAT my_json_format
  TYPE = JSON
  COMPRESSION = AUTO
  STRIP_OUTER_ARRAY = TRUE;

-- Load data
COPY INTO events (event_data)
FROM @my_s3_stage
FILE_FORMAT = (FORMAT_NAME = my_json_format)
ON_ERROR = 'SKIP_FILE';
Output
Number of rows loaded: 1000
Number of rows skipped: 0
Number of errors: 0
⚠ File Format Settings Matter
📊 Production Insight
In production, set ON_ERROR = 'CONTINUE' and log errors to a separate table for later inspection. Use VALIDATION_MODE during initial setup.
🎯 Key Takeaway
Use COPY INTO with appropriate file format settings to load semi-structured data. Always validate with a small sample first.

Querying JSON and Nested Data with Dot Notation

Snowflake provides two ways to access fields in a VARIANT: dot notation (:) and bracket notation ([]). Dot notation is simpler for fixed field names.

Example JSON: { "user": { "name": "Alice", "age": 30 }, "items": ["book", "pen"] }

Query: ``sql SELECT event_data:user.name::VARCHAR AS user_name, event_data:user.age::INT AS user_age, event_data:items[0]::VARCHAR AS first_item FROM events; ``

Note the double colon (::) for explicit casting. Without casting, the result remains VARIANT, which may cause issues in joins or aggregations.

For fields with special characters or spaces, use bracket notation with quotes: ``sql SELECT event_data['user name']::VARCHAR FROM events; ``

You can also use the GET function: ``sql SELECT GET(event_data, 'user'):name::VARCHAR FROM events; ``

Performance tip: Explicit casting (::) is faster than relying on implicit conversion.

query_json.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Sample data
INSERT INTO events (event_data)
SELECT PARSE_JSON('{"user":{"name":"Alice","age":30},"items":["book","pen"]}');

-- Query using dot notation
SELECT 
  event_data:user.name::VARCHAR AS user_name,
  event_data:user.age::INT AS user_age,
  event_data:items[0]::VARCHAR AS first_item
FROM events;
Output
USER_NAME | USER_AGE | FIRST_ITEM
----------+----------+-----------
Alice | 30 | book
💡Always Cast to Native Types
📊 Production Insight
In production, avoid deeply nested dot notation (e.g., a:b:c:d). It's slower and harder to read. Consider flattening the structure into columns using a view.
🎯 Key Takeaway
Use dot notation (:) for simple field access and bracket notation for complex keys. Always cast extracted values to native types.

Flattening Arrays with FLATTEN and LATERAL FLATTEN

When your data contains arrays, you often need to expand each array element into its own row. Snowflake's FLATTEN function does exactly that.

Example: Each event has an array of 'items'. We want one row per item.

``sql SELECT event_id, f.value::VARCHAR AS item FROM events, LATERAL FLATTEN(input => event_data:items) f; ``

The LATERAL keyword allows the FLATTEN to reference columns from the left side of the join. The output includes a 'value' column containing each array element.

FLATTEN also provides
  • SEQ: sequence number of the row
  • KEY: for objects, the key name
  • PATH: the path to the element
  • INDEX: the index of the element in the array

To include rows where the array is empty or NULL, use OUTER => TRUE: ``sql SELECT event_id, f.value::VARCHAR AS item FROM events, LATERAL FLATTEN(input => event_data:items, OUTER => TRUE) f; ``

This returns the event even if the array is empty (with NULL item).

flatten_array.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
-- Sample data with array
INSERT INTO events (event_data)
SELECT PARSE_JSON('{"user":"Bob","items":["laptop","mouse"]}');

-- Flatten items
SELECT 
  event_id,
  f.value::VARCHAR AS item,
  f.index
FROM events,
LATERAL FLATTEN(input => event_data:items) f;
Output
EVENT_ID | ITEM | INDEX
---------+--------+------
1 | laptop | 0
1 | mouse | 1
🔥FLATTEN vs LATERAL FLATTEN
📊 Production Insight
Be careful with deeply nested arrays; multiple FLATTENs can cause exponential row explosion. Use WHERE clauses to limit data early.
🎯 Key Takeaway
Use LATERAL FLATTEN to expand arrays into rows. Use OUTER => TRUE to preserve rows with empty arrays.

Working with Parquet and Avro Data

Parquet and Avro are columnar storage formats commonly used in data lakes. Snowflake can query them directly using the same VARIANT approach, but with some differences.

Parquet: Snowflake automatically infers the schema and maps columns to VARIANT. You can query nested fields using dot notation, but Parquet's schema is more rigid than JSON.

Example: Loading Parquet files. ```sql CREATE FILE FORMAT my_parquet_format TYPE = PARQUET;

COPY INTO my_table FROM @stage FILE_FORMAT = (FORMAT_NAME = my_parquet_format); ```

Avro: Similar to Parquet, but Avro files contain the schema within the file. Snowflake reads the schema and maps fields to VARIANT.

For both formats, you can use the INFER_SCHEMA function to get the column definitions: ``sql SELECT * FROM TABLE( INFER_SCHEMA( LOCATION => '@stage', FILE_FORMAT => 'my_parquet_format' ) ); ``

This helps you understand the structure before querying.

infer_schema.sqlSQL
1
2
3
4
5
6
7
-- Infer schema from Parquet files
SELECT * FROM TABLE(
  INFER_SCHEMA(
    LOCATION => '@my_stage',
    FILE_FORMAT => 'my_parquet_format'
  )
);
Output
COLUMN_NAME | TYPE | NULLABLE
------------+------------+---------
id | NUMBER(38,0) | YES
name | VARCHAR(16777216) | YES
data | VARIANT | YES
💡Use INFER_SCHEMA for Large Files
📊 Production Insight
Parquet files often have many columns. Consider creating a view that extracts only the needed fields to simplify queries and improve performance.
🎯 Key Takeaway
Parquet and Avro files are loaded similarly to JSON. Use INFER_SCHEMA to discover the schema before querying.

Performance Optimization for Semi-Structured Queries

Querying VARIANT columns can be slower than querying native columns due to the overhead of parsing and type conversion. Here are best practices:

  1. Explicit Casting: Always cast extracted values to native types (::VARCHAR, ::INT). This avoids repeated parsing.
  2. Materialized Views: If you frequently query the same field, create a materialized view that extracts it into a native column.
  3. ```sql
  4. CREATE MATERIALIZED VIEW mv_user_names AS
  5. SELECT event_id, event_data:user.name::VARCHAR AS user_name
  6. FROM events;
  7. ```
  8. Limit Nested Access: Deeply nested paths (a:b:c:d) are slower. Flatten the structure if possible.
  9. Use WHERE Clauses Early: Filter rows before extracting fields to reduce the amount of data parsed.
  10. Avoid SELECT *: Only select the fields you need.
  11. Use PARSE_JSON vs TRY_PARSE_JSON: TRY_PARSE_JSON returns NULL for invalid JSON, which is safer but slightly slower. Use PARSE_JSON when data is guaranteed valid.
  12. Compression: Snowflake compresses VARIANT data automatically, but you can further optimize by using appropriate file formats (Parquet is more compressed than JSON).
materialized_view.sqlSQL
1
2
3
4
5
6
7
-- Create materialized view for frequently accessed field
CREATE MATERIALIZED VIEW mv_user_names AS
SELECT event_id, event_data:user.name::VARCHAR AS user_name
FROM events;

-- Query the materialized view
SELECT * FROM mv_user_names WHERE user_name IS NOT NULL;
Output
EVENT_ID | USER_NAME
---------+----------
1 | Alice
2 | Bob
⚠ Materialized Views Have Costs
📊 Production Insight
Monitor query performance using QUERY_HISTORY. Look for queries with high 'bytes scanned' or 'partitions scanned' on VARIANT columns.
🎯 Key Takeaway
Optimize semi-structured queries by casting explicitly, using materialized views, and filtering early. Avoid deeply nested paths.

Handling Missing Fields and NULLs Gracefully

Semi-structured data often has missing fields or NULL values. Snowflake provides functions to handle these gracefully.

  • TRY_PARSE_JSON: Returns NULL instead of error if JSON is invalid.
  • GET: Returns NULL if key doesn't exist.
  • COALESCE: Replace NULL with a default.
  • IFNULL: Similar to COALESCE for two arguments.

Example: Safely extract a field that may be missing. ``sql SELECT COALESCE(event_data:missing_field::VARCHAR, 'default') AS safe_field FROM events; ``

For arrays, use ARRAY_SIZE to check if an array exists: ``sql SELECT CASE WHEN ARRAY_SIZE(event_data:items) > 0 THEN event_data:items[0]::VARCHAR ELSE NULL END AS first_item FROM events; ``

You can also use FLATTEN with OUTER => TRUE to include rows with empty arrays.

Remember: NULL in VARIANT is different from SQL NULL. Use IS NULL and IS NOT NULL to check.

null_handling.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- Safe extraction with COALESCE
SELECT 
  COALESCE(event_data:missing_field::VARCHAR, 'N/A') AS field
FROM events;

-- Check array existence
SELECT 
  CASE WHEN ARRAY_SIZE(event_data:items) > 0 
       THEN event_data:items[0]::VARCHAR 
       ELSE NULL 
  END AS first_item
FROM events;
Output
FIELD
-----
N/A
...
FIRST_ITEM
----------
book
NULL
🔥NULL vs 'null'
📊 Production Insight
In production, log the rate of NULL values for critical fields. A sudden increase may indicate a data source issue.
🎯 Key Takeaway
Always handle missing fields with COALESCE or IFNULL. Use ARRAY_SIZE to check array existence before accessing elements.

Advanced: Querying Nested Objects and Arrays

Real-world data often has deeply nested structures. Snowflake allows you to navigate these with a combination of dot notation, bracket notation, and FLATTEN.

Example: A JSON object with nested arrays of objects. ``json { "orders": [ { "order_id": 1, "items": [ {"product": "A", "price": 10}, {"product": "B", "price": 20} ] } ] } ``

To get all products and prices: ``sql SELECT o.value:order_id::INT AS order_id, i.value:product::VARCHAR AS product, i.value:price::INT AS price FROM events, LATERAL FLATTEN(input => event_data:orders) o, LATERAL FLATTEN(input => o.value:items) i; ``

This uses two FLATTENs: first to expand orders, then to expand items within each order.

You can also use the PATH and INDEX columns to understand the structure.

For very deep nesting, consider using a recursive CTE, but be cautious of performance.

nested_flatten.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
-- Sample data
INSERT INTO events (event_data)
SELECT PARSE_JSON('{"orders":[{"order_id":1,"items":[{"product":"A","price":10},{"product":"B","price":20}]}]}');

-- Flatten nested arrays
SELECT 
  o.value:order_id::INT AS order_id,
  i.value:product::VARCHAR AS product,
  i.value:price::INT AS price
FROM events,
LATERAL FLATTEN(input => event_data:orders) o,
LATERAL FLATTEN(input => o.value:items) i;
Output
ORDER_ID | PRODUCT | PRICE
---------+---------+------
1 | A | 10
1 | B | 20
💡Use Aliases for Clarity
📊 Production Insight
Avoid more than 2-3 levels of FLATTEN in a single query. Consider creating intermediate tables or views to simplify.
🎯 Key Takeaway
For deeply nested structures, chain multiple LATERAL FLATTENs. Use aliases to keep queries readable.
● Production incidentPOST-MORTEMseverity: high

The Missing NULL Check: A Snowflake Pipeline Meltdown

Symptom
Data pipeline jobs failed intermittently with 'Error parsing JSON' messages. Some days ran fine, others failed.
Assumption
The developer assumed all JSON fields were always present and non-null.
Root cause
A new data source occasionally sent JSON objects with missing fields, causing TRY_PARSE_JSON to return NULL, which was then used in a WHERE clause that expected a non-null value.
Fix
Added COALESCE and explicit NULL checks in the query: WHERE COALESCE(data:field, '') != ''.
Key lesson
  • Always handle NULLs in semi-structured data explicitly.
  • Use TRY_PARSE_JSON instead of PARSE_JSON to avoid hard failures.
  • Test with edge cases: missing fields, null values, empty arrays.
  • Implement monitoring for NULL rates in VARIANT columns.
  • Use FLATTEN with OUTER => TRUE to preserve rows with empty arrays.
Production debug guideSymptom to Action4 entries
Symptom · 01
Query returns NULL for a field that should have a value
Fix
Check if the field path is correct using GET() or : notation. Verify the data type with TYPEOF(). Use TRY_PARSE_JSON to see if the JSON is valid.
Symptom · 02
FLATTEN returns no rows for an array
Fix
Ensure the column is actually an array (TYPEOF). Use FLATTEN with OUTER => TRUE to include rows with empty arrays. Check for NULL values.
Symptom · 03
Performance degradation on VARIANT queries
Fix
Use explicit casting (::VARCHAR) instead of repeated dot notation. Create a materialized view if querying the same path often. Limit the number of nested FLATTENs.
Symptom · 04
Error: 'Invalid JSON' during COPY INTO
Fix
Use VALIDATION_MODE = 'RETURN_ERRORS' to identify bad rows. Pre-process with TRY_PARSE_JSON and filter out NULLs.
★ Quick Debug Cheat SheetCommon symptoms and immediate actions for Snowflake semi-structured data issues.
Field returns NULL
Immediate action
Check field path and data type
Commands
SELECT TYPEOF(data:field) FROM table;
SELECT GET(data, 'field') FROM table;
Fix now
Use COALESCE(data:field, 'default')
FLATTEN returns nothing+
Immediate action
Verify it's an array and use OUTER
Commands
SELECT TYPEOF(data) FROM table;
SELECT f.value FROM table, LATERAL FLATTEN(input => data, OUTER => TRUE) f;
Fix now
Add OUTER => TRUE
Slow query+
Immediate action
Use explicit casting
Commands
SELECT data:field::VARCHAR FROM table;
EXPLAIN SELECT ...;
Fix now
Create materialized view on extracted fields
COPY INTO fails+
Immediate action
Validate files
Commands
COPY INTO table FROM @stage VALIDATION_MODE = 'RETURN_ERRORS';
SELECT * FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));
Fix now
Use TRY_PARSE_JSON and filter NULLs
FeatureVARIANTOBJECTARRAY
Can store objectsYesYesNo
Can store arraysYesNoYes
Can store scalarsYesNoNo
Schema enforcementNoneMust be objectMust be array
Use caseFlexible storageStrict object structureStrict array structure
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
create_variant_table.sqlCREATE TABLE events (What is Semi-Structured Data and VARIANT?
load_json.sqlCREATE FILE FORMAT my_json_formatLoading Semi-Structured Data into Snowflake
query_json.sqlINSERT INTO events (event_data)Querying JSON and Nested Data with Dot Notation
flatten_array.sqlINSERT INTO events (event_data)Flattening Arrays with FLATTEN and LATERAL FLATTEN
infer_schema.sqlSELECT * FROM TABLE(Working with Parquet and Avro Data
materialized_view.sqlCREATE MATERIALIZED VIEW mv_user_names ASPerformance Optimization for Semi-Structured Queries
null_handling.sqlSELECTHandling Missing Fields and NULLs Gracefully
nested_flatten.sqlINSERT INTO events (event_data)Advanced

Key takeaways

1
Snowflake's VARIANT type allows flexible storage and querying of semi-structured data like JSON, Parquet, and Avro.
2
Use dot notation (:) for simple field access and bracket notation ([]) for complex keys; always cast to native types.
3
LATERAL FLATTEN is essential for expanding arrays into rows; use OUTER => TRUE to preserve rows with empty arrays.
4
Optimize performance with explicit casting, materialized views, early filtering, and avoiding deeply nested paths.
5
Handle missing fields and NULLs gracefully using TRY_PARSE_JSON, COALESCE, and ARRAY_SIZE.

Common mistakes to avoid

5 patterns
×

Forgetting to cast extracted VARIANT values to native types

×

Using PARSE_JSON instead of TRY_PARSE_JSON for untrusted data

×

Not using OUTER => TRUE in FLATTEN when you need to preserve rows with empty arrays

×

Assuming all JSON fields are present and non-null

×

Overusing deeply nested dot notation (e.g., a:b:c:d:e)

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the VARIANT data type in Snowflake and when would you use it?
Q02SENIOR
Explain the difference between dot notation (:) and bracket notation ([]...
Q03SENIOR
How would you flatten a nested array of objects in Snowflake? Provide an...
Q04SENIOR
What performance optimizations can you apply when querying VARIANT colum...
Q05SENIOR
How does Snowflake handle schema evolution for semi-structured data?
Q01 of 05JUNIOR

What is the VARIANT data type in Snowflake and when would you use it?

ANSWER
VARIANT is a flexible data type that can store semi-structured data like JSON, Avro, Parquet, and XML. You use it when the schema is not fixed or when ingesting data from multiple sources with varying structures.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between VARIANT, OBJECT, and ARRAY in Snowflake?
02
How do I handle invalid JSON in Snowflake?
03
Can I create indexes on VARIANT columns?
04
How do I query a specific key in a JSON array of objects?
05
What is the maximum size of a VARIANT value?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 17, 2026
last updated
2,439
articles · all by Naren
🔥

That's Snowflake. Mark it forged?

5 min read · try the examples if you haven't

Previous
Cost Optimization: Managing Credits, Warehouses, and Storage
13 / 33 · Snowflake
Next
Snowpipe and Snowpipe Streaming: Automated Continuous Data Ingestion