Spark JSON and Nested Data
JSON files describe records with fields inside fields and lists inside lists. Web services, mobile apps, and event trackers produce this format all day long. Spark reads JSON into nested columns and gives you simple tools to open them up into flat tables.
The Layman View
Think of a parcel that contains smaller boxes, and each small box holds items. Nested data follows that pattern. A flat table is a shelf where every item sits in the open. Spark helps you unpack the boxes onto the shelf.
Sample JSON File
{"id": 1, "name": "Asha", "address": {"city": "Pune", "zip": "411001"}, "skills": ["spark", "sql"]}
{"id": 2, "name": "Ben", "address": {"city": "Delhi", "zip": "110001"}, "skills": ["python"]}
Each line holds one complete record. Spark calls this the JSON Lines layout and reads it by default.
Reading JSON
df = spark.read.json("people.json")
df.printSchema()
root
|-- address: struct
| |-- city: string
| |-- zip: string
|-- id: long
|-- name: string
|-- skills: array
| |-- element: string
A file that stores one large JSON array or pretty-printed records across many lines needs the multiLine option.
df = spark.read.option("multiLine", True).json("big_array.json")
Reaching Inside a Struct
Use dot notation to read a field inside a struct column.
from pyspark.sql import functions as F
df.select("name", "address.city", F.col("address.zip").alias("zip")).show()
name city zip
Asha Pune 411001
Ben Delhi 110001
Flattening Arrays with explode
The explode function turns each item of an array into its own row. Other columns repeat beside each item.
df.select("name", F.explode("skills").alias("skill")).show()
Before explode After explode
name skills name skill
Asha [spark, sql] Asha spark
Ben [python] ---> Asha sql
Ben python
Rows with an empty or null array vanish from the result. Use explode_outer to keep those rows with a null value.
Counting Items Without Exploding
df.select("name", F.size("skills").alias("skill_count"),
F.array_contains("skills", "spark").alias("knows_spark")).show()
Parsing JSON Stored in a Text Column
Many tables keep raw JSON inside a string column. The from_json function converts the text into a struct when you supply a schema.
from pyspark.sql.types import StructType, StructField, StringType, IntegerType
raw = spark.createDataFrame([('{"device":"phone","clicks":7}',)], ["payload"])
payload_schema = StructType([
StructField("device", StringType()),
StructField("clicks", IntegerType())
])
parsed = raw.withColumn("data", F.from_json("payload", payload_schema))
parsed.select("data.device", "data.clicks").show()
Building Nested Output
The struct and to_json functions reverse the process.
out = df.select("id", F.to_json(F.struct("name", "address")).alias("json_text"))
out.show(truncate=False)
Writing Flat Results
flat = (df.select("id", "name", "address.city", F.explode("skills").alias("skill")))
flat.write.mode("overwrite").parquet("/data/people_flat")
Good Habits
- Supply a schema for large JSON inputs. Inference scans the whole dataset.
- Flatten data once, store it as Parquet, and run later queries on the flat copy.
- Check for null structs before you read their fields.
- Keep an eye on explode. Each exploded array multiplies the row count.
Nested Arrays of Structs
Orders often hold a list of item records. Each item carries its own fields. Explode the array first and read the struct fields after.
orders = spark.read.json("orders.json")
# {"order_id": 10, "items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}]}
lines = (orders
.select("order_id", F.explode("items").alias("item"))
.select("order_id", "item.sku", "item.qty"))
lines.show()
order_id items order_id sku qty
10 [{A1,2}, {B7,1}] ---> 10 A1 2
10 B7 1
Each order now spreads across one row per item. A groupBy on sku gives total quantities per product.
Dealing with Changing Fields
Event producers add new fields over time. Spark fills missing fields with null when records disagree. Old files that lack a new field still load without errors, and the new column simply stays empty for those rows. Review the printed schema after each new data delivery so you notice additions early.
