Menggunakan data JSON dengan mssql-python

Microsoft SQL 2016 dan versi yang lebih baru serta Azure SQL menyediakan dukungan JSON melalui fungsi yang beroperasi pada nvarchar kolom. Driver mssql-python mengirim dan menerima JSON sebagai string Python biasa. Kamu bisa:

  • Simpan JSON sebagai string dalam nvarchar kolom.
  • Kueri JSON dengan ekspresi jalur menggunakan JSON_VALUE, JSON_QUERY, dan OPENJSON.
  • Mengubah data relasional ke JSON dengan FOR JSON.
  • Mengurai JSON ke dalam format relasional dengan OPENJSON.

Note

Microsoft SQL menyimpan data JSON dalam nvarchar kolom, bukan dalam jenis kolom JSON khusus. Driver mssql-python mengirim dan menerima JSON sebagai string biasa. Gunakan modul bawaan Python json untuk melakukan serialisasi dan deserialisasi di sisi klien.

Simpan data JSON

Serialisasi dikte Python ke string dengan json.dumps() sebelum dimasukkan ke dalam kolom nvarchar.

Sisipkan string JSON

Simpan kamus Python sebagai teks JSON dalam tabel database:

import json
import mssql_python

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes"
)
cursor = conn.cursor()

# Create table with JSON column
cursor.execute("""
    CREATE TABLE #JsonProducts (
        ProductID INT IDENTITY PRIMARY KEY,
        Name NVARCHAR(100),
        JsonData NVARCHAR(MAX)
    )
""")

# Python dict to JSON string
product_data = {
    "name": "Widget Pro",
    "specs": {
        "weight": 2.5,
        "dimensions": {"width": 10, "height": 5, "depth": 3}
    },
    "tags": ["electronics", "gadgets", "bestseller"]
}

cursor.execute("""
    INSERT INTO #JsonProducts (Name, JsonData)
    VALUES (%(name)s, %(json)s)
""", {"name": "Widget Pro", "json": json.dumps(product_data)})
conn.commit()

Validasi JSON saat penyisipan

Gunakan fungsi untuk ISJSON() memvalidasi sintaks JSON sebelum menyisipkan:

data = {"name": "Widget", "specs": {"weight": 1.5}}

cursor.execute("""
    CREATE TABLE #JsonValidate (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonValidate (Name, JsonData)
    SELECT %(name)s, %(json)s
    WHERE ISJSON(%(json)s) = 1
""", {"name": "Widget", "json": json.dumps(data)})

if cursor.rowcount == 0:
    raise ValueError("Invalid JSON data")

Kueri data JSON

Gunakan fungsi jalur JSON Microsoft SQL untuk mengekstrak nilai di server sebelum mengembalikannya ke klien.

Ekstrak nilai skalar

Gunakan JSON_VALUE untuk mengekstrak nilai tunggal:

# Create table with sample JSON data
cursor.execute("""
    CREATE TABLE #JsonExtract (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonExtract (Name, JsonData) VALUES (
        'Widget Pro',
        '{"name":"Widget Pro","specs":{"weight":2.5,"dimensions":{"width":10,"height":5,"depth":3}},"tags":["electronics","gadgets"]}'
    )
""")

cursor.execute("""
    SELECT 
        Name,
        JSON_VALUE(JsonData, '$.specs.weight') AS Weight,
        JSON_VALUE(JsonData, '$.specs.dimensions.width') AS Width
    FROM #JsonExtract
    WHERE JSON_VALUE(JsonData, '$.name') = %(name)s
""", {"name": "Widget Pro"})

row = cursor.fetchone()
print(f"Weight: {row.Weight}, Width: {row.Width}")

Ekstrak objek atau array

Gunakan JSON_QUERY untuk objek dan array.

cursor.execute("""
    CREATE TABLE #JsonQuery (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonQuery (Name, JsonData) VALUES (
        'Widget Pro',
        '{"specs":{"weight":2.5,"color":"blue"},"tags":["electronics","gadgets"]}'
    )
""")

cursor.execute("""
    SELECT 
        Name,
        JSON_QUERY(JsonData, '$.specs') AS Specs,
        JSON_QUERY(JsonData, '$.tags') AS Tags
    FROM #JsonQuery
""")

for row in cursor:
    specs = json.loads(row.Specs) if row.Specs else {}
    tags = json.loads(row.Tags) if row.Tags else []
    print(f"{row.Name}: {specs}, Tags: {tags}")

Mengurai larik JSON menjadi baris

Perluas array JSON menjadi baris menggunakan OPENJSON:

cursor.execute("""
    CREATE TABLE #JsonArray (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonArray (Name, JsonData) VALUES 
        ('Widget Pro', '{"tags":["electronics","gadgets","bestseller"]}'),
        ('Gadget X', '{"tags":["tools","gadgets"]}')
""")

cursor.execute("""
    SELECT p.Name, t.value AS Tag
    FROM #JsonArray p
    CROSS APPLY OPENJSON(p.JsonData, '$.tags') t
""")

for row in cursor:
    print(f"Product: {row.Name}, Tag: {row.Tag}")

Mengurai objek JSON ke kolom

Ekstrak bidang individual dari objek JSON menggunakan JSON_VALUE() dan mentransmisikan hasilnya ke jenis SQL yang sesuai.

cursor.execute("""
    CREATE TABLE #JsonCols (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonCols (Name, JsonData) VALUES (
        'Widget Pro',
        '{"name":"Widget Pro","specs":{"weight":2.5,"dimensions":{"width":10,"height":5}}}'
    )
""")

cursor.execute("""
    SELECT 
        p.ProductID,
        j.name AS ProductName,
        j.weight,
        j.width,
        j.height
    FROM #JsonCols p
    CROSS APPLY OPENJSON(p.JsonData)
    WITH (
        name NVARCHAR(100) '$.name',
        weight DECIMAL(5,2) '$.specs.weight',
        width INT '$.specs.dimensions.width',
        height INT '$.specs.dimensions.height'
    ) j
""")

Mengubah data JSON

Gunakan JSON_MODIFY untuk memperbarui jalur tertentu dalam dokumen JSON tanpa menulis ulang seluruh nilai.

Perbarui nilai JSON

Ubah satu properti JSON menggunakan JSON_MODIFY:

cursor.execute("""
    CREATE TABLE #JsonMod (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonMod (Name, JsonData) VALUES (
        'Widget Pro',
        '{"specs":{"weight":2.5,"dimensions":{"width":10}},"tags":["electronics"]}'
    )
""")

cursor.execute("""
    UPDATE #JsonMod
    SET JsonData = JSON_MODIFY(JsonData, '$.specs.weight', %(weight)s)
    WHERE ProductID = %(id)s
""", {"weight": 3.0, "id": 1})
conn.commit()

Tambahkan properti JSON

Sisipkan properti baru ke dalam objek JSON yang ada:

cursor.execute("""
    CREATE TABLE #JsonAdd (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonAdd (Name, JsonData) VALUES (
        'Widget Pro', '{"specs":{"weight":2.5}}'
    )
""")

cursor.execute("""
    UPDATE #JsonAdd
    SET JsonData = JSON_MODIFY(JsonData, '$.specs.color', %(color)s)
    WHERE ProductID = %(id)s
""", {"color": "blue", "id": 1})

Menghapus properti JSON

Hapus properti dari objek JSON dengan mengaturnya ke NULL:

cursor.execute("""
    CREATE TABLE #JsonRem (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonRem (Name, JsonData) VALUES (
        'Widget Pro', '{"specs":{"weight":2.5,"color":"blue"}}'
    )
""")

cursor.execute("""
    UPDATE #JsonRem
    SET JsonData = JSON_MODIFY(JsonData, '$.specs.color', NULL)
    WHERE ProductID = %(id)s
""", {"id": 1})

Tambahkan ke array JSON

Tambahkan nilai baru ke akhir array JSON menggunakan append direktif di JSON_MODIFY:

cursor.execute("""
    CREATE TABLE #JsonAppend (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonAppend (Name, JsonData) VALUES (
        'Widget Pro', '{"tags":["electronics","gadgets"]}'
    )
""")

cursor.execute("""
    UPDATE #JsonAppend
    SET JsonData = JSON_MODIFY(
        JsonData, 
        'append $.tags', 
        %(tag)s
    )
    WHERE ProductID = %(id)s
""", {"tag": "new-arrival", "id": 1})

Mengonversi data relasional ke JSON

Klausa mengubah FOR JSON hasil kueri menjadi string JSON di sisi server.

UNTUK JSON AUTO

Hasilkan JSON dari hasil kueri:

cursor.execute("""
    SELECT TOP 5 o.SalesOrderID, p.LastName AS CustomerName, o.TotalDue
    FROM Sales.SalesOrderHeader o
    JOIN Sales.Customer c ON o.CustomerID = c.CustomerID
    JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
    FOR JSON AUTO
""")

# Result is a single string containing JSON
json_result = cursor.fetchval()
orders = json.loads(json_result)
print(json.dumps(orders, indent=2))

UNTUK JALUR JSON

Dapatkan kontrol lebih besar atas struktur JSON:

cursor.execute("""
    SELECT 
        o.SalesOrderID AS 'order.id',
        o.OrderDate AS 'order.date',
        p.LastName AS 'customer.name',
        e.EmailAddress AS 'customer.email'
    FROM Sales.SalesOrderHeader o
    JOIN Sales.Customer c ON o.CustomerID = c.CustomerID
    JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
    JOIN Person.EmailAddress e ON p.BusinessEntityID = e.BusinessEntityID
    WHERE o.SalesOrderID = %(id)s
    FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
""", {"id": 43659})

json_result = cursor.fetchval()
order = json.loads(json_result)
# Structure: {"order": {"id": 43659, "date": "..."}, "customer": {"name": "...", "email": "..."}}

JSON bersarang

Kuerikan struktur data dengan array dan objek JSON berlapis menggunakan subkueri dengan FOR JSON untuk menghasilkan output JSON hierarkis.

cursor.execute("""
    SELECT TOP 3
        c.CustomerID,
        p.LastName AS CustomerName,
        (SELECT TOP 3 o.SalesOrderID, o.TotalDue
         FROM Sales.SalesOrderHeader o
         WHERE o.CustomerID = c.CustomerID
         FOR JSON PATH) AS Orders
    FROM Sales.Customer c
    JOIN Person.Person p ON c.PersonID = p.BusinessEntityID
    WHERE c.PersonID IS NOT NULL
    FOR JSON PATH
""")

json_result = cursor.fetchval()
customers = json.loads(json_result)
# Each customer has nested Orders array

Pola integrasi Python

Pola ini menunjukkan cara membangun abstraksi Python melalui tabel yang didukung JSON.

Pola repositori dengan JSON

Terapkan lapisan akses data yang menserialkan dan mendeserialisasi objek Python ke kolom JSON, menyediakan antarmuka yang aman untuk database.

from dataclasses import dataclass, asdict
from typing import Optional
import json

@dataclass
class ProductSpecs:
    weight: float
    color: str
    dimensions: dict

@dataclass
class Product:
    id: Optional[int]
    name: str
    specs: ProductSpecs

class ProductRepository:
    def __init__(self, connection):
        self.conn = connection
        cursor = self.conn.cursor()
        cursor.execute("""
            IF OBJECT_ID('#JsonRepo') IS NULL
                CREATE TABLE #JsonRepo (
                    ProductID INT IDENTITY PRIMARY KEY,
                    Name NVARCHAR(100),
                    JsonData NVARCHAR(MAX)
                )
        """)
        self.conn.commit()
    
    def save(self, product: Product) -> int:
        cursor = self.conn.cursor()
        specs_json = json.dumps(asdict(product.specs))
        
        if product.id:
            cursor.execute("""
                UPDATE #JsonRepo SET Name = %(name)s, JsonData = %(json)s
                WHERE ProductID = %(id)s
            """, {"name": product.name, "json": specs_json, "id": product.id})
        else:
            cursor.execute("""
                INSERT INTO #JsonRepo (Name, JsonData)
                OUTPUT INSERTED.ProductID
                VALUES (%(name)s, %(json)s)
            """, {"name": product.name, "json": specs_json})
            product.id = cursor.fetchval()
        
        self.conn.commit()
        return product.id
    
    def get(self, product_id: int) -> Optional[Product]:
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT ProductID, Name, JsonData FROM #JsonRepo WHERE ProductID = %(id)s
        """, {"id": product_id})
        
        row = cursor.fetchone()
        if row is None:
            return None
        
        specs_data = json.loads(row.JsonData)
        return Product(
            id=row.ProductID,
            name=row.Name,
            specs=ProductSpecs(**specs_data)
        )

Sambungkan ke database, lalu buat repositori dan gunakan untuk menyimpan dan mengambil produk. Metode save() memilih cabang INSERT ketika id adalah None dan cabang UPDATE jika tidak:

conn = mssql_python.connect(connection_string)

repo = ProductRepository(conn)

# id is None, so save() inserts a new row and returns the generated ProductID.
product = Product(
    id=None,
    name="Widget Pro",
    specs=ProductSpecs(weight=2.5, color="black", dimensions={"width": 10, "height": 5})
)
product_id = repo.save(product)
print(f"Saved product {product_id}")

# Read the product back into a typed Product object.
loaded = repo.get(product_id)
print(loaded)

conn.close()

Repositori membuat #JsonRepo sebagai tabel sementara lokal yang cakupannya terbatas pada koneksi yang Anda teruskan, jadi save() dan get() harus menggunakan koneksi yang sama. Tabel dihapus saat koneksi ditutup.

Menangani hasil JSON besar secara efisien

Jika hasil JSON berukuran besar, ambil dalam beberapa bagian pada beberapa baris.

def fetch_json_in_parts(cursor, query: str, params: dict) -> list:
    """Handle JSON results that might span multiple rows."""
    cursor.execute(query, params)
    
    # FOR JSON might split large results across rows
    json_parts = []
    for row in cursor:
        json_parts.append(row[0])
    
    # Combine parts
    json_string = "".join(json_parts)
    return json.loads(json_string) if json_string else []

# Usage
data = fetch_json_in_parts(cursor, "SELECT TOP 100 * FROM Production.Product FOR JSON AUTO", {})

Mengonversi hasil kueri ke JSON di Python

Ubah hasil kueri relasional ke dalam format JSON di Python dengan mengonversi setiap baris ke kamus, lalu serialisasi ke JSON.

def query_to_json(cursor, query: str, params: dict = None) -> str:
    """Execute query and return results as JSON string."""
    cursor.execute(query, params or {})
    columns = [col[0] for col in cursor.description]
    
    rows = []
    for row in cursor:
        rows.append(dict(zip(columns, row)))
    
    return json.dumps(rows, default=str, indent=2)

# Usage
json_output = query_to_json(cursor, "SELECT TOP 5 ProductID, Name, ListPrice FROM Production.Product WHERE ProductSubcategoryID = %(cat)s", {"cat": 1})
print(json_output)

Pengindeksan data JSON

Buat kolom komputasi yang didukung oleh ekspresi jalur JSON untuk membuat jalur dapat diindeks.

Kolom yang dihitung dengan indeks

Tentukan kolom komputasi yang mengekstrak nilai JSON dan terapkan indeks padanya untuk pemfilteran yang efisien pada jalur JSON yang sering dikueri. Contoh berikut membuat tabel permanen, menambahkan kolom komputasi yang tetap ada di atas jalur JSON $.specs.weight , dan membuat indeks di atasnya.

cursor.execute("""
    IF OBJECT_ID('dbo.ProductCatalog', 'U') IS NOT NULL
        DROP TABLE dbo.ProductCatalog
""")
cursor.execute("""
    CREATE TABLE dbo.ProductCatalog (
        ProductID   INT IDENTITY PRIMARY KEY,
        Name        NVARCHAR(100),
        JsonData    NVARCHAR(MAX)
    )
""")

# Insert sample rows with JSON data
rows = [
    ("Widget Pro",   '{"specs":{"weight":2.5,"color":"blue"}}'),
    ("Gadget X",     '{"specs":{"weight":0.8,"color":"red"}}'),
    ("Heavy Duty",   '{"specs":{"weight":9.1,"color":"gray"}}'),
]
cursor.executemany(
    "INSERT INTO dbo.ProductCatalog (Name, JsonData) VALUES (%(name)s, %(json)s)",
    [{"name": n, "json": j} for n, j in rows]
)
conn.commit()

# Add a persisted computed column that extracts weight from JSON
cursor.execute("""
    ALTER TABLE dbo.ProductCatalog
    ADD ProductWeight AS CAST(JSON_VALUE(JsonData, '$.specs.weight') AS DECIMAL(5,2)) PERSISTED
""")

# Index the computed column for efficient range queries
cursor.execute("""
    CREATE INDEX IX_ProductCatalog_Weight
    ON dbo.ProductCatalog (ProductWeight)
""")
conn.commit()

Kueri menggunakan kolom komputasi yang diindeks

Filter langsung berdasarkan kolom terhitung. Mesin kueri menggunakan indeks alih-alih memindai dan mengurai setiap dokumen JSON.

cursor.execute("""
    SELECT Name, ProductWeight
    FROM dbo.ProductCatalog
    WHERE ProductWeight > %(min_weight)s
    ORDER BY ProductWeight
""", {"min_weight": 1.0})

for row in cursor:
    print(f"{row.Name}: {row.ProductWeight} kg")

# Cleanup
cursor.execute("DROP TABLE dbo.ProductCatalog")
conn.commit()

Pilih antara kolom relasional dan penyimpanan JSON

Gunakan kolom relasional ketika data memiliki skema tetap, membutuhkan integritas referensial, berpartisipasi dalam JOIN, atau sering muncul dalam klausa WHERE. Gunakan kolom JSON (nvarchar(max)) saat data jarang, bervariasi di seluruh baris, atau mewakili konfigurasi atau metadata yang fleksibel.

Kapan menggunakan pemrosesan JSON sisi server versus sisi klien

Gunakan fungsi JSON Microsoft SQL (JSON_VALUE, JSON_QUERY, OPENJSON) saat Anda perlu memfilter, mengindeks, atau menggabungkan di seluruh bidang JSON tanpa mengambil setiap baris ke klien. Pilihan ini tepat ketika hanya subset baris yang cocok dengan kriteria Anda, atau ketika Anda menginginkan indeks kolom yang dihitung pada jalur JSON.

Gunakan pemrosesan Python sisi klien (json.loads()) saat Anda mengambil seluruh dokumen dan memprosesnya dalam logika aplikasi. Pendekatan ini bekerja dengan baik saat Anda memerlukan dokumen lengkap dan tidak memfilter bidang JSON dalam database.

Alur kerja gaya dokumen

Saat aplikasi Anda menyimpan dan mengambil seluruh dokumen, gunakan serialisasi sisi Python dan perlakukan kolom JSON sebagai penyimpanan buram. Proses dan kueri dokumen di Python dengan mengambil dan mendeserialisasi blob JSON lengkap:

import json

# Create the settings table
cursor.execute("""
    CREATE TABLE #Settings (
        UserID INT PRIMARY KEY,
        ConfigJson NVARCHAR(MAX)
    )
""")

# Store a configuration document
config = {
    "theme": "dark",
    "notifications": {"email": True, "sms": False},
    "custom_fields": {"department": "Engineering", "cost_center": "CC-100"}
}

cursor.execute(
    "INSERT INTO #Settings (UserID, ConfigJson) VALUES (%(uid)s, %(cfg)s)",
    {"uid": 1, "cfg": json.dumps(config)}
)

# Retrieve and process in Python
cursor.execute("SELECT ConfigJson FROM #Settings WHERE UserID = %(uid)s", {"uid": 1})
row = cursor.fetchone()
config = json.loads(row.ConfigJson)
print(config["notifications"]["email"])  # True

Kueri JSON di sisi server

Gunakan fungsi JSON Microsoft SQL saat Anda perlu memfilter, mengindeks, atau menggabungkan di seluruh bidang JSON tanpa mengambil setiap baris. Pendekatan ini lebih efisien daripada memuat semua baris ke Python untuk memfilter dalam memori:

  • JSON_VALUE mengekstrak nilai skalar dan dapat mengembalikan indeks kolom yang dihitung.
  • JSON_QUERY Mengekstrak objek dan array.
  • OPENJSON mengurai JSON menjadi baris untuk JOIN dan agregasi.
  • JSON_MODIFY memperbarui jalur tertentu tanpa menulis ulang seluruh dokumen.
# Filter by a JSON field server-side
cursor.execute("""
    SELECT UserID, ConfigJson
    FROM #Settings
    WHERE JSON_VALUE(ConfigJson, '$.custom_fields.department') = %(dept)s
""", {"dept": "Engineering"})

Untuk jalur JSON yang sering dikueri, buat kolom komputasi dengan indeks:

ALTER TABLE Settings
ADD Department AS JSON_VALUE(ConfigJson, '$.custom_fields.department');

CREATE INDEX IX_Settings_Department ON Settings(Department);

Praktik terbaik

Terapkan panduan ini untuk menggunakan kolom JSON dengan andal.

Memvalidasi JSON sebelum penyimpanan

Validasi pengidentifikasi JSON dan tabel/kolom sebelum menyimpan untuk mencegah serangan injeksi.

def store_json_safely(cursor, table: str, json_column: str, data: dict):
    """Store JSON with validation."""
    # Validate identifiers to prevent SQL injection
    import re
    if not re.match(r'^[A-Za-z_][A-Za-z0-9_.]*$', table):
        raise ValueError(f"Invalid table name: {table}")
    if not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', json_column):
        raise ValueError(f"Invalid column name: {json_column}")

    json_str = json.dumps(data)
    
    # Check if valid JSON in Microsoft SQL
    cursor.execute("SELECT ISJSON(%(json)s)", {"json": json_str})
    if cursor.fetchval() != 1:
        raise ValueError("Invalid JSON")
    
    cursor.execute(f"INSERT INTO {table} ({json_column}) VALUES (%(json)s)", {"json": json_str})

Jangan terlalu banyak menggunakan JSON

Gunakan kolom JSON untuk data fleksibel atau jarang, seperti preferensi pengguna atau bidang kustom. Gunakan kolom relasional untuk:

  • Data yang sering diakses.
  • Data yang membutuhkan integritas referensial.
  • Kolom yang digunakan dalam klausa WHERE.

Tangani None/NULL dengan benar

Tangani bidang JSON yang hilang atau opsional dengan menyisipkan nilai NULL untuk kolom yang tidak memiliki data.

cursor.execute("""
    CREATE TABLE #JsonOpt (
        ProductID INT IDENTITY, Name NVARCHAR(100), JsonData NVARCHAR(MAX)
    )
""")
cursor.execute("""
    INSERT INTO #JsonOpt (Name, JsonData) VALUES (
        'Widget Pro', '{"required_field":"value"}'
    )
""")

cursor.execute("""
    SELECT 
        Name,
        JSON_VALUE(JsonData, '$.optional_field') AS OptionalValue
    FROM #JsonOpt
""")

for row in cursor:
    # JSON_VALUE returns NULL if path doesn't exist
    value = row.OptionalValue or "default"
    print(f"{row.Name}: {value}")