Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

json_type

Prev Next

Applies to VAST Cluster 5.5.

Functions › Parsing and JSON functions › Check and inspect. Returns the type of a JSON document, or of the value at a path in it, as a name such as OBJECT, ARRAY or VARCHAR.

Syntax

json_type(json)
json_type(json, path)
json_type(json, paths)
Argument Type Description
json VARCHAR or JSON The JSON document.
path VARCHAR A JSON path, JSON pointer or top-level key. Without it, json_type returns the type of the whole document.
paths VARCHAR[] A constant list of paths.

Returns: VARCHAR. A list of paths, or a path with a wildcard, returns VARCHAR[].

Set up the examples

The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_json_json_type. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.

This block connects, defines the run() and plan() helpers, and creates a table of four events with a JSON payload column: two objects, an array and a NULL. DOC is a sample document for the examples that don't read the table:

import os
import re

import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb

BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_json_json_type"'

conn = dbapi.connect(
    driver=adbc_driver_vastdb.get_driver_path(),
    db_kwargs={
        "vast.db.endpoint": os.environ["QE_ENDPOINT"],
        "vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
        "vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
    autocommit=True,
)

def server_message(e):
    """The server's message in a driver error, without the request ID or the echoed statement."""
    text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
    found = (re.search(r'message: "(.*?)", request_id', text, re.S)
             or re.search(r"description: (.*)\)", text, re.S))
    message = found.group(1) if found else text.strip().splitlines()[-1]
    message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
    message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
    return message.replace(BUCKET, "<bucket>")

def run(sql):
    """Run one statement. Print the rows it returns, or the error."""
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            rows = cur.fetchall() if cur.description else []
    except (adbc_driver_manager.Error, OSError) as e:
        print(f"{type(e).__name__}: {server_message(e)}")
    else:
        print(rows)

def plan(sql):
    """Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
    with conn.cursor() as cur:
        cur.execute("EXPLAIN " + sql)
        steps = cur.fetchall()[0][1]
    print("\n".join(line for line in steps.splitlines() if "Rows" not in line))

TABLE = f"{SCHEMA}.events"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, payload VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, '{\"user\": \"ada\", \"n\": 3, \"tags\": [\"a\", \"b\"], \"geo\": {\"city\": \"Oslo\", \"lat\": 59.9}}'), "
    "(2, '{\"user\": \"alan\", \"n\": \"many\", \"tags\": [], \"geo\": null}'), "
    "(3, '[10, 20, [30, 40]]'), "
    "(4, NULL)")

DOC = """'{"user": "ada", "n": 3, "tags": ["a", "b", "c"], "geo": {"city": "Oslo", "lat": 59.9}, "z": null}'"""
[]
[]
[(4,)]

What it does

json_type parses the document, follows the path if you give one, and names the type of what it finds:

JSON value Result
An object, {...} 'OBJECT'
An array, [...] 'ARRAY'
A string, "ada" (also one that looks like a date) 'VARCHAR'
An integer of 0 or more, 3 'UBIGINT'
A negative integer, -1, and -0 'BIGINT'
A number with a fraction or an exponent, 1.5, 1e2; an integer too big for UBIGINT 'DOUBLE'
true 'BOOLEAN'
null 'NULL' (text, not SQL NULL)
run(f"SELECT json_type({DOC}), json_type({DOC}, '$.geo'), json_type({DOC}, '$.tags'), json_type({DOC}, '$.user'), "
    f"json_type({DOC}, '$.z')")
run("SELECT json_type('{\"a\": 3}', '$.a'), json_type('{\"a\": -1}', '$.a'), json_type('{\"a\": -0}', '$.a'), "
    "json_type('{\"a\": 1.5}', '$.a'), json_type('{\"a\": 1e2}', '$.a')")
run("SELECT json_type('{\"a\": 18446744073709551616}', '$.a'), json_type('{\"a\": true}', '$.a'), "
    "json_type('{\"a\": \"2026-10-16\"}', '$.a')")
[('OBJECT', 'OBJECT', 'ARRAY', 'VARCHAR', 'NULL')]
[('UBIGINT', 'BIGINT', 'BIGINT', 'DOUBLE', 'DOUBLE')]
[('DOUBLE', 'BOOLEAN', 'VARCHAR')]

NULLs and edge cases

Input Result
A NULL document or a NULL path NULL
A path that isn't in the document ('$.missing') NULL
A wildcard path ('$.tags[*]') one type per element: ['VARCHAR', 'VARCHAR', 'VARCHAR']
A list of paths one type per path, NULL for a missing one
A bare scalar document: '"s"', '42', 'true', 'null' 'VARCHAR', 'UBIGINT', 'BOOLEAN', 'NULL'
'not json' Error: Malformed JSON at byte 0 of input: invalid literal. Input: not json
An integer path, json_type(json, 0) Error: No function matches the given name and argument types; write '$[0]'

The same cases, as the cluster returns them:

run(f"SELECT json_type(NULL), json_type({DOC}, NULL), json_type({DOC}, '$.missing'), json_type({DOC}, '$.tags[*]')")
run(f"SELECT json_type({DOC}, ['$.geo', '$.n', '$.missing'])")
run("SELECT json_type('\"s\"'), json_type('42'), json_type('true'), json_type('null')")
run("SELECT json_type('not json')")
run(f"SELECT json_type({DOC}, 0)")
[(None, None, None, ['VARCHAR', 'VARCHAR', 'VARCHAR'])]
[(['OBJECT', 'UBIGINT', None],)]
[('VARCHAR', 'UBIGINT', 'BOOLEAN', 'NULL')]
OSError: Malformed JSON at byte 0 of input: invalid literal.  Input: not json
InternalError: Binder Error: No function matches the given name and argument types 'json_type(STRING_LITERAL, INTEGER_LITERAL)'. You might need to add explicit type casts.

Examples

The type of each document and of two fields. In row 2, n is a string and geo is JSON null:

run(f"SELECT id, json_type(payload), json_type(payload, '$.n'), json_type(payload, '$.geo') "
    f"FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'OBJECT', 'UBIGINT', 'OBJECT'), (2, 'OBJECT', 'VARCHAR', 'NULL'), (3, 'ARRAY', None, None), (4, None, None, None)]

Keep the documents that are arrays:

run(f"SELECT id FROM {TABLE} WHERE json_type(payload) = 'ARRAY' ORDER BY id LIMIT 10")
[(3,)]

Check a field's type before you cast it. TRY_CAST gives the same NULL for row 2's "many", without telling you why:

run(f"SELECT id, CASE WHEN json_type(payload, '$.n') IN ('UBIGINT', 'BIGINT') "
    f"THEN (payload ->> '$.n')::BIGINT END FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 3), (2, None), (3, None), (4, None)]

How it runs in VAST

The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). VAST DataBase stores a JSON document as text and can't look inside it, so json_type is always computed by the Query Engine:

  • In the SELECT list, the scan reads the whole payload column and the Query Engine parses each document it returns.
  • In WHERE, the scan sends payload for every row, and a FILTER step in the Query Engine parses each document and keeps the rows that match. No rows are skipped in storage.
  • Every document it reads must be valid JSON. The first invalid one stops the query with Malformed JSON ... (see invalid JSON in a column).

The plan of the WHERE example shows the FILTER step above the scan:

plan(f"SELECT id FROM {TABLE} WHERE json_type(payload) = 'ARRAY'")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│   (json_type(payload) =   │
│          'ARRAY')         │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│          payload          │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Store the type if you filter on it often. A VARCHAR column such as payload_kind, set when you write the row, is evaluated in VAST DataBase; json_type(payload) = 'ARRAY' parses every row in the Query Engine.
  • Narrow the rows with a pushed-down condition. A condition such as id = 1 is evaluated in VAST DataBase, so a query that fails on the whole table because of one invalid document can still run on the rows it keeps.

Limitations and errors

  • Number types follow the JSON text, not the value. 1e2 is 'DOUBLE' though it's a whole number, and -0 is 'BIGINT'. Check for all three number types (UBIGINT, BIGINT, DOUBLE) when you only need to know it's a number.
  • Paths are text. There's no integer form; write an array position as '$[0]'.
  • Invalid JSON stops the query. Use json_valid to find invalid documents first.
  • A list of paths must be constant. A list from a column fails with List of paths must be constant.
  • The document must be text. A number fails with No function matches the given name and argument types, for example 'json_type(INTEGER_LITERAL)'; a BLOB fails with Malformed JSON .... A DATE, BOOLEAN or list is not rejected (it returns 'VARCHAR', 'BOOLEAN' and 'ARRAY'); pass text.
run("SELECT json_type(123)")
run("SELECT json_type('\\xAA'::BLOB)")
InternalError: Binder Error: No function matches the given name and argument types 'json_type(INTEGER_LITERAL)'. You might need to add explicit type casts.
OSError: Malformed JSON at byte 0 of input: input length is 0.  Input: 

Similar functions

Function '{"a": 1}' '[1, "a"]'
json_type 'OBJECT' 'ARRAY'
json_keys ['a'] []
json_array_length 0 2
json_structure '{"a":"UBIGINT"}' '["JSON"]' (mixed types)
typeof 'VARCHAR': the SQL type of the argument, not of the JSON in it 'VARCHAR'
run("SELECT json_type('{\"a\": 1}'), json_keys('{\"a\": 1}'), json_array_length('{\"a\": 1}'), "
    "json_structure('{\"a\": 1}'), typeof('{\"a\": 1}'::VARCHAR)")
run("SELECT json_type('[1, \"a\"]'), json_keys('[1, \"a\"]'), json_array_length('[1, \"a\"]'), "
    "json_structure('[1, \"a\"]'), typeof('[1, \"a\"]'::VARCHAR)")
[('OBJECT', ['a'], 0, '{"a":"UBIGINT"}', 'VARCHAR')]
[('ARRAY', [], 2, '["JSON"]', 'VARCHAR')]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also