Skip to content

Schema

The schema block decides how API responses become columns.

mode Result
strict The default. Typed columns from the OpenAPI schema. Nested objects are flattened to flatten-depth; deeper ones become JSON strings.
variant The whole record as one native Spark VARIANT column.

Strict

Strict mode uses the OpenAPI schema to produce typed columns. Nested objects are flattened to flatten-depth levels. Anything deeper becomes a JSON string, which parse_json() can read if you need it.

Columns come from the schema of each endpoint's 200 (or default) response, under application/json, application/json with parameters such as ; charset=utf-8, any +json type such as application/hal+json, or */*, in that order of preference. A table whose endpoint has no such response fails when it's queried, naming the content types the spec does offer: in strict mode it would have no columns. Variant mode needs no schema, so there the table still loads. A source reading NDJSON takes its columns from the record under the NDJSON response instead; see Response formats.

Every column is nullable, including fields the spec marks required. A required field can still be null in the data: inside an optional parent object that's missing, when it's also declared nullable: true, or when the API breaks its own spec. A non-nullable column would let Spark drop IS NULL filters on it, and with them those rows.

Column types

OpenAPI Spark
integer with format: int32 int
integer with any other format, or none bigint
number double
boolean boolean
string with format: date date
string with format: date-time timestamp
any other string string
an array, or an object deeper than flatten-depth string, holding the JSON
an object with no declared properties, such as a map given only by additionalProperties string, holding the JSON

An integer without a format has no size limit in OpenAPI, and IDs and epoch milliseconds routinely pass the 32-bit range, so only an explicit int32 is an int. A bigint holds any value up to 2^63 − 1.

Columns come from the fields a spec declares. A field the API sends but the spec doesn't declare is skipped, including when the spec allows extra fields with additionalProperties.

Values that don't match their column

APIs don't always send what their spec says. A value with exactly one reading in its column's type is converted:

Column Converted Example
int, bigint a string holding an integer "42" → 42
double a string holding a number "3.5" → 3.5
boolean "true" or "false", in any case "TRUE" → true
timestamp a space in place of T "2024-01-02 03:04:05Z"
timestamp no zone, read as UTC "2024-01-02T03:04:05"

Anything else that doesn't fit or doesn't parse becomes NULL, rather than failing the read. That includes values that would need a guess:

  • a fraction in an integer column, as a number or a string: 4.2, "3.0". A JSON number with nothing after the point, such as 3.0, is read as 3.
  • an int32 value out of range. The column's type is fixed when the query is planned.
  • an epoch number in a timestamp column. Seconds and milliseconds would both give a plausible date.
  • an empty string, or any other text, that isn't a date or timestamp.

The first such NULL in each column, in each task, is logged as a warning, naming the column and the value. Every one is counted: the scan's node in the SQL tab of the Spark UI shows values replaced with NULL, and values converted beside it, so you can see what was touched. A JSON null or a missing field is NULL without being counted.

array-handling controls array fields:

array-handling Result
keep_array The default. Each array is one column holding the array as a JSON string. Read elements with from_json or parse_json(), or use explode_view for a row per element.
explode_view Each array field also becomes a table, <table>_<field>, with one row per element. SHOW TABLES lists only these exploded tables.
both SHOW TABLES lists the base tables and the exploded ones.

Variant

schema {
  mode = "variant"
}
-- One column, `value`, of type VARIANT
SELECT value FROM api.default.users LIMIT 5;

-- variant_get() reads typed fields, with no parse_json needed
SELECT
  variant_get(value, '$.name', 'STRING') AS name,
  variant_get(value, '$.email', 'STRING') AS email
FROM api.default.users;

-- Nested paths work too
SELECT
  variant_get(value, '$.address.city', 'STRING') AS city,
  variant_get(value, '$.stats[0].value', 'INT') AS first_stat
FROM api.default.users;

Note

The colon syntax (value:name::string) in Databricks' documentation is Databricks-specific. Open-source Spark uses variant_get().

examples/pokeapi/pokeapi-variant.conf is a working variant-mode config.

Parent-child tables aren't supported in variant mode: loading one fails, saying so. The source's other tables still read. Use strict mode for a source with joins.