Skip to content
CityParquet is v0.1.0-draft — the specification is still changing.
Esc
↑↓navigate↵open⌘Jpreview
On this page

Extensions

CityGML ADEs and CityJSON Extensions: typed where possible, every added name carrying its extension's namespace.

The CityGML CM is extensible: a CityGML Application Domain Extension (ADE), or the equivalent CityJSON Extension, may add new feature types, new semantic surface types, and new attributes. CityParquet encodes each mechanism into its columnar model without losing the source data, preferring typed columns where the extension is regular and an escape hatch where it is not (see the extensions decision).

Each extension is identified by a short namespace, and every name the extension adds to a package carries this namespace as a prefix. The examples on this page use the Energy ADE 3.0 (beta 7; Agugiaro and Padsala, tudelft3d/Energy_ADE), whose namespace is energy. It is split into Core, Building Physics, Occupant Behaviour, Material and Construction, and Energy Systems modules; the examples draw on its Building Physics module.

The namespace

Every extension a package uses has a namespace that:

  • MUST match ^[a-z][a-z0-9]*$ — a lower-case letter followed by lower-case letters and digits, with no _; and
  • MUST be unique within the package: a namespace denotes the same extension in every file of the package.

The prefix form of a namespace is the namespace followed by _ (energy_). Because a namespace contains no _, the first _ of a prefixed name always ends its namespace.

Derivation. A writer derives the namespace from the source:

  • CityJSON Extension — the extension’s name, which is its key in the document’s extensions member, lower-cased, with every character outside [a-z0-9] removed: "Energy" → energy, "Noise" → noise, "Energy ADE" → energyade.
  • CityGML ADE — the prefix the source document first binds to the ADE’s XML namespace, normalised the same way; ade when that leaves nothing or a digit first, and numbered from 2 (uro2) when an ADE the document uses earlier has it, so that every ADE of a document has its own.

A writer MAY instead accept a namespace supplied by the operator for an extension; the result MUST still satisfy the two rules above. A writer MUST reject the input (a hard error) when a derivation yields an empty namespace, a namespace that does not start with a letter, or a namespace that collides with another extension’s.

What carries the prefix. Every name the extension adds:

Name Source Stored as
Extended attribute (a column name) +heatCapacity energy_heatCapacity
Extended semantic surface type (geometry_properties_lod*.surfaces[].type) +PartyWallSurface energy_PartyWallSurface
Extended city-object class (object_type) +ThermalZone energy_ThermalZone
Module an extension declares (a file name) Building Physics module energy_building_physics.parquet
The extension’s own file (a file name) — energy.parquet

Core module files and core names are never prefixed.

Extended feature types

An extension — a CityGML ADE, or the equivalent CityJSON Extension — groups its feature types into modules, so they follow the same by-module rule as core types. Within a file the object_type column distinguishes each row’s class, and an extension class’s object_type is its class name with the namespace prefix (energy_ThermalZone).

The ModuleKey. Every class routes to a file via a single key: a core CityGML 3.0 module (Building, Transportation, …), an owned extension module — the module the extension itself declares for that class — or the extension’s own file. This is one uniform mechanism, not a special case bolted onto the by-module rule:

  • A class belonging to a core module gets the module’s file, exactly as any core type does.
  • An extension class that declares its own module gets a file named after that module: the namespace prefix followed by the module name in snake_case (the file-name rule of core modules). The Energy ADE’s ThermalZone and ThermalBoundary both belong to its Building Physics module, so both are stored in energy_building_physics.parquet, distinguished by object_type (energy_ThermalZone, energy_ThermalBoundary).
  • An extension class that specialises an existing class (declares no module of its own, but subclasses a core or extension class) routes to that class’s module file — an extension class specialising Building lands in building.parquet, exactly as BuildingPart does. This holds recursively. A CityJSON Extension states the class it specialises in its extraCityObjects schema, by an allOf member whose $ref names a definition of CityJSON’s cityobjects.schema.json ("$ref": "cityobjects.schema.json#/_AbstractBuilding"): a City Object type routes to its module, _AbstractBuilding to Building’s, _AbstractTransportationComplex to Transportation’s, and _AbstractCityObject or another definition that names no class in particular (ExtensionObject) to none. A CityGML ADE states it in its schema, by the element’s substitutionGroup (see the CityJSON form of ADE content).
  • An extension class that declares neither a module of its own nor a class it specialises — a CityJSON Extension class (CityJSON has no modules) whose schema names no definition or only _AbstractCityObject, and a CityGML ADE class whose schema is not read or substitutes it for no CityGML class CityJSON has a definition for — gets its extension’s own file, named by the namespace alone: the urf ADE’s UseDistrict and FirePreventionDistrict are both stored in urf.parquet. A namespace contains no _, so this file never collides with an extension module’s file.
  • Routing is indifferent to CityJSON’s + marker. The + is a detail of CityJSON’s own encoding of extension types, not a precondition for being one: an ADE class with no + and a resolvable declared module routes by that module like any other. A +-marked class whose name, without the +, is a core class name (+Building) routes to that core class’s module file, but it remains an extension class, so its object_type carries the prefix (energy_Building) and stays distinct from the core class’s.
  • A class with no resolvable ModuleKey at all — not a recognised core CityGML class, and not a class of any declared extension — is a hard error. Routing is total: every class the writer encounters must resolve to some file, or the input is rejected.

The file of a row is therefore its class’s module: the declaration records the class an extension class specialises, never its file.

🟡 Carrying full ADE object-class metadata (beyond the class name, its module file and the extension declaration below) remains an open question.

Extended semantic surfaces

An extended semantic surface type is stored with the namespace prefix in the type of its surface object inside the geometry_properties_lod* surfaces value: the Energy ADE’s +PartyWallSurface is stored as { "type": "energy_PartyWallSurface" }. Semantic surfaces are never promoted to column names, but they carry the prefix all the same, so one naming rule identifies every extension name in a package and keeps the names of different extensions apart.

Extended attributes

An extended attribute becomes a typed attribute column like any other, so it gets the same predicate and projection pushdown as a core attribute. CityJSON writes these with a leading + (+heatCapacity). Parquet itself imposes no restriction on a leading + in a column name, but such a name is hostile to SQL and to many query engines (it cannot be referenced unquoted, and reads as an operator). The column name is therefore the attribute name with the namespace prefix: the heatCapacity attribute of an Energy ADE ThermalZone is the column energy_heatCapacity.

The extended attribute columns are listed in the attributes metadata key like any other attribute; the prefix, resolved through city.extensions, identifies the extension each one belongs to.

The extension declaration: city.extensions

city.extensions (metadata key) declares the extensions a file uses. It is a JSON object keyed by namespace; each value references the extension’s schema document rather than embedding it:

"extensions": {
  "energy": {
    "name": "Energy",
    "url": "https://…/energy.ext.json",
    "version": "3.0"
  }
}
Field Req. Description
name ✔ The extension’s name in the source: the key of the CityJSON extensions member, or the ADE’s name.
url ✔ The extension’s schema document — a JSON Schema for a CityJSON Extension, an XSD for a CityGML ADE.
version The extension’s version, when the source states one.
xmlns CityGML ADE only: the ADE’s XML namespace URI.
xmlAttributes CityGML ADE only: the XML attributes that every occurrence of an ADE element carries with one value, by element local name, then attribute name ({"width": {"uom": "m"}}); see CityGML ADE content.
extraCityObjects, extraSemanticSurfaces, extraRootProperties, extraAttributes The names the extension adds, as a CityJSON Extension document lists them (extraAttributes by City Object type), when the source states them with the declaration; kept verbatim.
substitutionGroups, containers, propertyOrder CityGML ADE only, when the ADE’s XML Schema was read: the CityGML element each ADE attribute, class and container substitutes for, as {namespace URI}local; the element a CityGML 3.0 adeOf… hook holds each attribute in; and the order of the attributes and geometry properties of each extended class and container. Where a CityGML writer puts the ADE’s elements; kept verbatim.

A file that carries any extension name — a prefixed attribute column, object_type value or semantic surface type — MUST declare that name’s namespace here. Every file of a package that declares a namespace MUST declare it with the same value.

Attributing names to an extension

A writer attributes each extension name in the source to exactly one declared extension:

  • A CityGML ADE name belongs to the ADE whose XML namespace it is in.
  • A CityJSON source that declares exactly one extension: every + name belongs to that extension.
  • A CityJSON source that declares several extensions: a + name belongs to the one extension that lists it among the names of its kind — an attribute in extraAttributes, a City Object type in extraCityObjects, a semantic surface type in extraSemanticSurfaces — in its declaration, or in its schema document if the writer reads it. A name that no declared extension lists as its kind, or that several do, cannot be attributed and is a hard error.
  • A + name in a CityJSON source that declares no extension is a hard error: CityJSON requires every extension used in a document to be declared in its extensions member.

Collisions

A core (non-extension) attribute, semantic surface type or class whose name starts with a declared namespace followed by _ is a hard error: on export it would be indistinguishable from an extension name. With the Energy ADE declared, a core attribute named energy_label is rejected.

Likewise, an extension module whose file name equals a core module’s file name (a namespace water with a module Body → water_body.parquet) is a hard error, and so is an extension’s own file that does (a namespace building with a class of its own → building.parquet).

Export

On export to CityJSON, a name that starts with a declared namespace prefix becomes CityJSON’s + form: energy_heatCapacity → +heatCapacity, energy_ThermalZone → +ThermalZone, energy_PartyWallSurface → +PartyWallSurface. The document’s extensions member is rebuilt from city.extensions: each entry’s name becomes the member key, with its url, version and every other field it carries. A name with no declared prefix is written unchanged. On export to CityGML, xmlns gives the XML namespace the ADE’s names are written in, and substitutionGroups, containers and propertyOrder where its elements go.

CityJSON form of CityGML ADE content

A CityGML ADE adds XML elements a CityJSON document has no member for. A writer reading CityGML gives them the CityJSON form below, and from there they follow the rules above.

  • The extension. Each ADE the document uses is declared under the prefix the document binds to its namespace (uro, see the namespace), with url the schema document xsi:schemaLocation names for it when that is an absolute URL, else the ADE’s published schema when the writer knows it, else the location as written, else the namespace URI; version the last segment of the namespace URI when that is a version number, else empty (CityJSON requires the member); xmlns the namespace URI; its classes, semantic surfaces and attributes in extraCityObjects, extraSemanticSurfaces and extraAttributes; and, when its schema was read, what the schema says of where its elements go in substitutionGroups, containers and propertyOrder.
  • The schema. A writer reads an ADE’s XML Schema when the document’s xsi:schemaLocation names it by a path relative to the document and that file exists (the PLATEAU archive’s ../../schemas/iur/uro/3.2/urbanObject.xsd), with the documents it includes or imports by a relative path. A schema named by URL need not be fetched. Without its schema an ADE is read by position alone, as below.
  • Classes and surfaces. An ADE element where a city object belongs (a cityObjectMember, a nesting property, a groupMember) is a City Object of type +<local name>; one directly inside a boundedBy is a semantic surface of type +<local name>. Their own lod* geometry properties are geometry, their CityGML properties attributes as for a core class, and their own ADE’s properties attributes named by local name without +. When the schema is read, a class’s extraCityObjects entry names the CityGML class its element substitutes for, through the ADE’s own abstract classes, as the CityJSON definition it corresponds to: uro:UndergroundBuilding substitutes for bldg:_AbstractBuilding, so it is {"allOf": [{"$ref": "cityobjects.schema.json#/_AbstractBuilding"}]} and routes to building.parquet. CityJSON has no abstract bridge, tunnel or water object, so a class specialising one names Bridge, Tunnel or WaterBody; one that reaches only the CityGML city object, or a CityGML class CityJSON has no definition for (veg:_VegetationObject), names _AbstractCityObject. Without the schema the entry is an empty object.
  • Extension properties. An ADE element directly inside an object of a CityGML class, or of another ADE’s class, is the attribute +<local name>.
  • Values. An element’s content becomes JSON: its child elements an object keyed by local name, prefixed <prefix>: when the child is in another namespace than its parent element, a repeated child an array; an empty element an empty object. Text takes the type the schema declares for the element in its context: an integer for xs:integer and its restrictions, a number for xs:double, xs:float, xs:decimal and the GML measures (gml:MeasureType, gml:LengthType, …), a boolean for xs:boolean, else a string. Without a declared type it is a string, or a number when the element has a uom attribute. Text that is not a value of its type stays a string. An XML attribute without a prefix that every occurrence of its element carries with one value is stated once, in the declaration’s xmlAttributes; an element with any other attribute becomes {"$": <value>, "@<attribute>": "…"}. Text between child elements, and GML geometry inside an ADE property, have no CityJSON form and are left out.

The Chiyoda-ku urf tile of PLATEAU thus becomes +FirePreventionDistrict and +UseDistrict City Objects in urf.parquet, with object_type urf_FirePreventionDistrict and an xs:integer floorAreaRate column of type BIGINT; a uro:roadStructureAttribute of a tran:Road the column uro_roadStructureAttribute of transportation.parquet; and the ubld tile’s uro:UndergroundBuilding a row of building.parquet with object_type uro_UndergroundBuilding.

Example

Constructed. A CityJSONSeq source declares one extension, Energy, and a Building carries a core attribute and an Energy ADE attribute. (The Energy ADE attaches heatCapacity to a ThermalZone; it sits on the building here so that one object shows every rule.)

{ "type": "CityJSON", "version": "2.0", "transform": { /* … */ },
  "extensions": { "Energy": { "url": "https://…/energy.ext.json", "version": "3.0" } },
  "CityObjects": {}, "vertices": [] }

{ "type": "CityJSONFeature", "id": "b-42",
  "CityObjects": {
    "b-42": { "type": "Building",
              "attributes": { "yearOfConstruction": 1998, "+heatCapacity": 120000.0 } } },
  "vertices": [] }

The source declares exactly one extension, so +heatCapacity belongs to it, and the namespace is the member key lower-cased, energy. The attribute becomes the column energy_heatCapacity, inferred DOUBLE, beside the core yearOfConstruction:

id object_type yearOfConstruction BIGINT energy_heatCapacity DOUBLE
b-42 Building 1998 120000.0

The footer of building.parquet declares the namespace, with the member key as its name, and lists both columns in attributes:

"attributes": ["yearOfConstruction", "energy_heatCapacity"],
"extensions": {
  "energy": { "name": "Energy", "url": "https://…/energy.ext.json", "version": "3.0" }
}

On export the energy_ prefix becomes + again, and the extensions member is rebuilt from city.extensions under the key Energy.

Last updated on October 15, 2026

Was this page helpful?