CityJSON Interop
July 9, 2026 · View on GitHub
For a task-oriented walkthrough (measuring buildings, validating solids, and cross-checking against 3DBAG ground truth on the remote Delft dataset), see EXAMPLE.md. This file covers the composition mechanics and how to run the interop tests.
The design doc §12 specifies that duckdb-3d integrates with the
duckdb-cityjson community
extension via plain SQL composition: cityjson produces WKB plus
geometry_properties JSON, and duckdb-3d consumes both through the 2-arg
ST_3DFromWKB overload.
Smoke test
test/sql/cityjson_interop.test covers the full pipeline against a tiny
unit-cube fixture (test/data/unit_cube.city.json).
The test is gated on require cityjson. The DuckDB sqllogic-test runner
treats community extensions as "excluded from autoloading", so under
make test_debug the test gracefully skips unless extra setup is in
place.
Running the smoke test manually
The simplest path — confirmed working — is to run the queries directly through the debug shell, which uses your user-wide extension install:
make debug # one-time
./build/debug/duckdb -unsigned -c \
"INSTALL cityjson FROM community;" # one-time
./build/debug/duckdb -unsigned -c "
LOAD cityjson;
LOAD three_d;
SELECT id,
ST_3DNumFaces(solid) AS faces,
ST_3DIsClosed(solid) AS closed,
ROUND(ST_3DSurfaceArea(solid), 6) AS area,
ROUND(ST_3DVolume(solid), 6) AS volume
FROM (
SELECT id, ST_3DFromWKB(geometry, geometry_properties) AS solid
FROM read_cityjson('test/data/unit_cube.city.json', lod => '2.2')
WHERE geometry IS NOT NULL
);
"
Expected:
id | faces | closed | area | volume
cube | 6 | true | 6.0 | 1.0
Remote CityJSONSeq smoke test
For remote CityJSONSeq data, pass lod => '...' so cityjson emits WKB
columns, and use ST_3DTryFromWKB while exploring real-world data:
ASAN_OPTIONS=detect_container_overflow=0 ./build/debug/duckdb -unsigned -c "
LOAD cityjson;
LOAD three_d;
WITH solids AS (
SELECT id, ST_3DTryFromWKB(geometry, geometry_properties) AS solid
FROM read_cityjsonseq(
'https://storage.googleapis.com/cityjson/delft.city.jsonl',
lod => '2.2'
)
WHERE geometry IS NOT NULL
)
SELECT id,
ST_3DNumFaces(solid) AS faces,
ST_3DIsClosed(solid) AS closed,
ROUND(ST_3DSurfaceArea(solid), 3) AS area
FROM solids
WHERE solid IS NOT NULL
LIMIT 10;
"
Running the smoke test under sqllogic
To make test/sql/cityjson_interop.test run under make test_debug, two
things must hold simultaneously when the runner starts:
DUCKDB_TEST_AUTOLOADING=available(or=all) — flips the runner from the default "no autoload" mode.- The cityjson extension binary must be discoverable by the runner. The
runner looks at
${LOCAL_EXTENSION_REPO}(env override) or otherwise atbuild/debug/repository. Either copy~/.duckdb/extensions/<duckdb-version>/<platform>/cityjson.duckdb_extension*intobuild/debug/repository/<duckdb-version>/<platform>/, or setLOCAL_EXTENSION_REPOto a directory laid out the same way.
Note that DuckDB refuses to re-INSTALL an extension whose origin doesn't
match the previously installed copy. If you've already
INSTALL cityjson FROM community, the runner's INSTALL ... FROM '<path>'
step will fail with "origin is different" and the test will skip. The
practical workaround is to run with a clean DuckDB home dir, for example
by exporting HOME=$(mktemp -d) for the test invocation.
Caveats
read_cityjson(path, lod => '...')is the per-LOD mode that emits ageometry BLOBplusgeometry_properties VARCHARcolumn. Without thelodparameter, cityjson emits aSTRUCTinstead andST_3DFromWKBcannot consume it — passlod => '...'always.- The same applies to CityJSONSeq inputs. Use
read_cityjsonseq(url_or_path, lod => '2.2')when you want WKB columns; plainread_cityjsonseq(url_or_path)returns LOD-specificgeom_lod*structs instead. - DuckDB debug builds may run with AddressSanitizer enabled. On macOS,
loading the release-built community
cityjsonextension can trip an ASancontainer-overflowcheck while opening remote files. If the stack trace points at DuckDB settings duringread_cityjsonseq('https://...'), rerun the debug shell withASAN_OPTIONS=detect_container_overflow=0. - The
geometry_propertiesJSON emitted by cityjson uses a numeric"type"field (a WKB type code) and the human-readable name lives in"cityjsonType".duckdb-3daccepts both forms. - Filter
WHERE geometry IS NOT NULLupstream — objects without the requested LOD have a NULL geometry and would propagate NULL through the constructor.