CityJSON Specifications 1.1.1

Living Standard,

This version:
https://cityjson.org/specs/1.1.1/
Latest published version:
https://cityjson.org/specs/
Previous Versions:
Feedback:
GitHub
Editors:
Hugo Ledoux (TU Delft)
Balázs Dukai (3DGI)

Abstract

CityJSON is a JSON-based encoding for a subset of the OGC CityGML data model (version 3.0). It defines how to store digital 3D models of cities and landscapes. The aim of CityJSON is to offer an alternative to the GML encoding of CityGML, which can be verbose and complex to read and manipulate. CityJSON aims at being easy-to-use, both for reading datasets and for creating them. It was designed with programmers in mind, so that tools and APIs supporting it can be quickly built.

1. CityJSON Object

A CityJSON object represents one 3D city model of a given area, this model may contain features of different types, as defined in the CityGML data model.

A CityJSON object:

The minimal valid CityJSON object is thus:

{
  "type": "CityJSON",
  "version": "1.1",
  "transform": {
    "scale": [0.0, 0.0, 0.0],
    "translate": [1.0, 1.0, 1.0]
  },
  "CityObjects": {},
  "vertices": []
}

An "empty" but complete CityJSON object will look like this:

{
  "type": "CityJSON",
  "version": "1.1",
  "extensions": {},
  "transform": {
    "scale": [0.0, 0.0, 0.0],
    "translate": [1.0, 1.0, 1.0]
  },
  "metadata": {},
  "CityObjects": {},
  "vertices": [],
  "appearance": {},
  "geometry-templates": {}
}
While the order of the member values of a CityJSON should preferably be as above, not all JSON generators allow one to do this, thus the order is not prescribed.

2. The different City Objects

City Objects 1st and 2nd levels

There are 2 kinds of City Objects:
  1. 1st-level: City Objects that can "exist by themselves".

  2. 2nd-level: City Objects that need to have a "parents" to exist.

This is because the schema of CityGML has been flattened out. For example, a "BuildingInstallation" cannot be present in a dataset without being the "children" of a "Building", but a "Building" can be present by itself.

A City Object:

A City Object of type 2nd-level:

"CityObjects": {
  "id-1": {
    "type": "Building",
    "geographicalExtent": [ 84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9 ], 
    "attributes": { 
      "measuredHeight": 22.3,
      "roofType": "gable",
      "owner": "Elvis Presley"
    },
    "children": ["id-2"],
    "geometry": [{...}]
  },
  "id-2": {
    "type": "BuildingPart", 
    "parents": ["id-1"],
    "children": ["id-3"],
    ...
  },
  "id-3": {
    "type": "BuildingInstallation", 
    "parents": ["id-2"],
    ...
  },
  "id-4": {
    "type": "LandUse", 
    ...
  }
}

A minimal valid City Object ("Building" in this case, but any 1st-level could apply) is:

{
  "type": "Building"
}

And a minimal 2nd-level valid City Object ("BuildingPart" in this case, but any 2nd-level could apply) is:

{
  "type": "BuildingPart", 
  "parents": ["id-parent"]
}

2.1. Attributes for all City Objects

The attributes of a given City Object are not prescribed (unlike in CityGML). This means that the "attributes" is a JSON object and its content is a JSON key-value pair ("owner" in the example above is one such attribute).

"CityObjects": {
  "id-1": {
    "type": "LandUse", 
    "attributes": { 
      "function": "Industry and Business",
      "area": "120m^2"
    },
    "geometry": [{...}]
  },
  "id-2": {
    "type": "WaterBody", 
    "attributes": { 
      "name": "Lake Black"
    },
    "geometry": [{...}]
  }
}

2.2. Bridge

Six City Objects are related to bridges:

The geometry of both "Bridge" and "BridgePart" can only be represented with these Geometry Objects: (1) "Solid", (2) "CompositeSolid", (3) "MultiSurface", (4) "CompositeSurface". The geometry of the four other objects can be represented with any of the Geometry Objects.

A City Object of type "Bridge" or "BridgePart" may have a member "address", whose value is an array of JSON objects listing the potentially several addresses of that bridge . The properties of an address JSON object are free, to accommodate the different ways addresses are described in different countries.

"CityObjects": {
  "LondonTower": {
    "type": "Bridge", 
    "address": [
      {
        "City": "London",
        "Country": "UK"
      }
    ],
    "children": ["Bext1", "Bext2", "Inst-2017-11-14"],
    "geometry": [{
      "type": "MultiSurface",
      "lod": "2",
      "boundaries": [
        [[0, 3, 2, 1]], 
        [[4, 5, 6, 7]], 
        [[0, 1, 5, 4]], 
        [[1, 2, 6, 5]], 
        [[2, 3, 7, 6]], 
        [[3, 0, 4, 7]]
      ]
    }]    
  }
}

2.3. Building

Eight City Objects are related to buildings:

The geometry of "Building", "BuildingPart", "BuildingStorey", "BuildingRoom", and "BuildingUnit" can only be represented with these Geometry Objects: (1) "Solid", (2) "CompositeSolid", (3) "MultiSurface", (4) "CompositeSurface".

All of the eight, except "Building", must have a "parents" property. The installations, furnitures, and subdivisions can have as parents a "Building", a "BuildingPart", or a "BuildingRoom".

The geometry of "BuildingInstallation", "BuildingConstructiveElement", or "BuildingFurniture" objects can be represented with any of the Geometry Objects.

A City Object of type "Building", "BuildingPart" or "BuildingUnit" may have a member "address", whose value is an array of JSON objects listing the potentially several addresses of that building (an apartment building could contain several for instance). The properties of an address JSON object are free, to accommodate the different ways addresses are described in different countries. If a location is necessary (eg to locate the position of the front door) then a property "location" should be used, and it should contain a "MultiPoint".

"CityObjects": {
  "id-1": {
    "type": "Building", 
    "attributes": { 
      "roofType": "gabled roof"
    },
    "geographicalExtent": [ 84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9 ],
    "children": ["id-56", "id-832", "mybalcony"]
  },
  "id-56": {
    "type": "BuildingPart", 
    "parents": ["id-1"],
    ...
  },
  "mybalcony": {
    "type": "BuildingInstallation", 
    "parents": ["id-1"],
    ...
  }
  ...
}
"myroom": {
  "type": "BuildingRoom", 
  "attributes": {
    "usage": "living room"
  },
  "parents": ["id-1"],
  "geometry": [{
    "type": "Solid",
    "lod": "2",
    "boundaries": [
      [ [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], ... ]
    ]
  }]    
}               
{
  "type": "Building", 
  "address": [
    {
      "Country": "Canada",
      "Locality": "Chibougamau",
      "ThoroughfareNumber": "1",
      "ThoroughfareName": "rue de la Patate",
      "Postcode": "H0H 0H0",
      "location": {
        "type": "MultiPoint",
        "lod": "1",
        "boundaries": [231]
      }
    }
  ]
}

2.4. CityFurniture

The geometry of a City Object of type "CityFurniture" can be any Geometry Object.

"mystopsign": {
  "type": "CityFurniture", 
  "attributes": { 
    "function": "bus stop"
  },
  "geometry": [{
    "type": "MultiSurface",
    "lod": "2",
    "boundaries": [
      [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]]
    ]
  }]
}

2.5. CityObjectGroup

The CityGML concept of groups, where City Objects are aggregated based on certain criteria (think of a neighbourhood in a city for instance), is possible in CityJSON. The group is a City Object, and it can contain, if needed, a geometry (the polygon representing the neighbourhood for instance).

Since a "CityObjectGroup" is also a City Object, it can be part of another group.

A City Object of type "CityObjectGroup":

"CityObjects": {
  "my-neighbourhood": {
    "type": "CityObjectGroup",
    "children": ["building1", "building2", "building666"]
  }
}
"CityObjects": {
  "my-neighbourhood": {
    "type": "CityObjectGroup",
    "attributes": {
      "location": "Chibougamau Sud"
    },
    "children": ["building1", "building666"],
    "children_roles": ["residential building", "voting location"],
    "geometry": [{
      "type": "MultiSurface",
      "lod": "2",
      "boundaries": [ [[2, 41, 5, 77]] ]
    }]
  }
}

2.6. LandUse

The geometry of a City Object of type "LandUse" can be of type "MultiSurface" or "CompositeSurface".

"oneparcel": {
  "type": "LandUse", 
  "geometry": [{
    "type": "MultiSurface",
    "lod": "1",
    "boundaries": [
      [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]]
    ]
  }]    
}

2.7. OtherConstruction

This is used for constructions that are not buildings, bridges, or tunnels. Examples are:

The geometry of a City Object of type "OtherConstruction" can be any Geometry Object.

"mypylon": {
  "type": "OtherConstruction", 
  "attributes": { 
    "class": "windmill",
    "conditionOfConstruction": "underConstruction"
  },
  "geometry": [{
    "type": "MultiSurface",
    "lod": "2",
    "boundaries": [
       [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], ...
    ]
  }] 
}

2.8. PlantCover

The geometry of a City Object of type "PlantCover" can be of type: (1) "Solid", (2) "CompositeSolid", (3) "MultiSolid", (4) "MultiSurface", (5) "CompositeSurface".

"myplants": {
  "type": "PlantCover", 
  "attributes": { 
    "averageHeight": 11.05
  },
  "geometry": [{
    "type": "MultiSolid",
    "lod": "2",
    "boundaries": [
      [
        [ [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], [[10, 13, 22, 31]] ]
      ],
      [
        [ [[5, 34, 31, 12]], [[44, 54, 62, 74]], [[111, 123, 922, 66]] ]
      ]  
    ]
  }]    
}

2.9. SolitaryVegetationObject

The geometry of a City Object of type "SolitaryVegetationObject" can be any Geometry Object.

"onebigtree": {
  "type": "SolitaryVegetationObject", 
  "attributes": { 
    "trunkDiameter": 5.3,
    "crownDiameter": 11.0
  },
  "geometry": [{
    "type": "MultiPoint",
    "lod": "1",
    "boundaries": [1]
  }]
}

2.10. TINRelief

The geometry of a City Object of type "TINRelief" can only be of type "CompositeSurface".

CityJSON does not define a specific Geometry Object for a TIN (triangulated irregular network), it is simply a CompositeSurface for which every surface is a triangle (thus a polygon having 3 vertices, and no interior ring).

Notice that in practice any "CompositeSurface" is allowed for encoding a terrain, and that arbitrary polygons could also be used (not just triangles).

"myterrain01": {
  "type": "TINRelief", 
  "geographicalExtent": [ 84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9 ],
  "geometry": [{
    "type": "CompositeSurface",
    "lod": "1",
    "boundaries": [
       [[0, 3, 2]], [[4, 5, 6]], [[1, 2, 6]], [[2, 3, 7]], [[3, 0, 4]]
    ]
  }]    
}

2.11. Transportation

Four City Objects are related to transportation:

Observe that the "Section", "Intersection", and "Track" classes from CityGML are omitted because they simply can be handled with specific attributes.

"ma_rue": {
  "type": "Road", 
  "attributes": {
    "class": "backwards",
    "clearanceSpace": 2.23,
    "clearanceSpaceUnit": "meter"
  },
  "children": ["sect1", "sect2"],
  "geometry": [...]
}
"sect1": {
  "type": "Road", 
  "attributes": {
    "class": "section"
  },
  "parents": ["ma_rue"],
  "geometry": [...],
}

Similarly, the CityGML classes "TrafficArea", "AuxiliaryTrafficArea", "Marking", and "Hole" are implemented as semantic surface (see § 3.3 Semantics of geometric primitives). That is, the surface representing a road should be split into sub-surfaces (therefore forming a "MultiSurface" or a "CompositeSurface"), and each of the sub-surfaces has semantics.

"ma_rue": {
  "type": "Road", 
  "geometry": [{
    "type": "MultiSurface",
    "lod": "2",
    "boundaries": [
       [[0, 3, 2, 1, 4]], [[4, 5, 6, 666, 12]], [[0, 1, 5]], [[20, 21, 75]]
    ]
  }],
  "semantics": {
    "surfaces": [
      {
        "type": "TrafficArea",
        "surfaceMaterial": ["asphalt"],
        "function": "road"
      },
      {
        "type": "AuxiliaryTrafficArea",
        "function": "green areas"
      },
      {
        "type": "TrafficArea",
        "surfaceMaterial": ["dirt"],
        "function": "road"
      }
    ],
    "values": [0, 1, null, 2]
  }
}

2.12. Tunnel

Six City Objects are related to tunnels:

The geometry of both "Tunnel" and "TunnelPart" can only be represented with these Geometry Objects: (1) "Solid", (2) "CompositeSolid", (3) "MultiSurface", (4) "CompositeSurface".

The geometry of the other four objects can be represented with any of the Geometry Objects.

"CityObjects": {
  "Lærdalstunnelen": {
    "type": "Tunnel", 
    "attributes": { 
      "yearOfConstruction": 2000,
      "length": "24.5km"
    },
    "children": ["stoparea1"],
    "geometry": [{
      "type": "Solid",
      "lod": "2",
      "boundaries": [
        [ [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]] ]
      ]
    }] 
  }
}

2.13. WaterBody

The geometry of a City Object of type "WaterBody" can be of types: "MultiLineString", "MultiSurface", "CompositeSurface", "Solid", or "CompositeSolid".

"mygreatlake": {
  "type": "WaterBody", 
  "attributes": {
    "usage": "leisure",
  },
  "geometry": [{
    "type": "Solid",
    "lod": "2",
    "boundaries": [
      [ [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]] ]
    ]
  }]    
}               

3. Geometry Objects

CityJSON defines the following 3D geometric primitives, all of which are embedded in 3D space (and therefore their vertices have (x, y, z) coordinates). The indexing mechanism of the format Wavefront OBJ is reused, that is a geometry does not store the locations of its vertices, but points to a vertex in a list (property "vertices" in the CityJSON Object).

As is the case in CityGML, only linear and planar primitives are allowed; no curves or parametric surfaces can be represented.

A Geometry object is a JSON object for which the type member’s value is one of the following:

  1. "MultiPoint"

  2. "MultiLineString"

  3. "MultiSurface"

  4. "CompositeSurface"

  5. "Solid"

  6. "MultiSolid"

  7. "CompositeSolid"

  8. "GeometryInstance" (this is another type with different properties, see § 3.4 Geometry templates)

A Geometry object:

There is no Geometry Object for MultiGeometry. Instead, for the "geometry" member of a CityObject, the different geometries may be enumerated in the array (all with the same value for the member "lod").

3.1. The coordinates of the vertices

A CityJSON must have one member named "vertices", whose value is an array of coordinates of each vertex of the city model. Their position in this array (0-based) is used to represent the Geometry Objects.

"vertices": [
  [0.0, 0.0, 0.0],
  [1.0, 0.0, 0.0],
  [0.0, 0.0, 0.0],
  ...
  [1.0, 0.0, 0.0],
  [8523.134, 487625.134, 2.03]
]

3.2. Arrays to represent boundaries

The depth of the hierarchy of arrays depends on the Geometry object, and is as follows.

JSON does not allow comments, the comments in the example below (C++ style: //-- my comments) are only to explain the cases, and should be removed.
{
  "type": "MultiPoint",
  "lod": "1",
  "boundaries": [2, 44, 0, 7]
}
{
  "type": "MultiLineString",
  "lod": "1",
  "boundaries": [
    [2, 3, 5], [77, 55, 212]
  ]  
}
{
  "type": "MultiSurface",
  "lod": "2",
  "boundaries": [
    [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]]
  ]
}
{
  "type": "Solid",
  "lod": "2",
  "boundaries": [
    //-- exterior shell
    [ [[0, 3, 2, 1, 22]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], [[1, 2, 6, 5]] ], 
    //-- interior shell
    [ [[240, 243, 124]], [[244, 246, 724]], [[34, 414, 45]], [[111, 246, 5]] ] 
  ]
}
{
  "type": "CompositeSolid",
  "lod": "3",
  "boundaries": [
    [ //-- 1st Solid
      [ [[0, 3, 2, 1, 22]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], [[1, 2, 6, 5]] ],
      [ [[240, 243, 124]], [[244, 246, 724]], [[34, 414, 45]], [[111, 246, 5]] ]
    ],
    [ //-- 2st Solid
      [ [[666, 667, 668]], [[74, 75, 76]], [[880, 881, 885]], [[111, 122, 226]] ] 
    ]    
  ]
}
See this tutorial for further explanation on the depth of arrays of Geometry objects.

3.3. Semantics of geometric primitives

A Semantic Object is a JSON object representing the semantics of a primitive of a geometry (e.g. a surface of a building). It may also represent other attributes of the primitive (e.g. the slope of the roof, or the solar potential). For surface and volumetric geometries (e.g. MultiSurface, Solid and MultiSolid), a primitive is a surface. If a geometry is a MultiPoint or a MultiLineString, then the primitives are its respective sub-parts: points and linestrings.

A Semantic Object:

{
  "type": "RoofSurface",
  "slope": 16.4,
  "children": [2, 37],
  "solar-potential": 5
}

{
  "type": "Window",
  "parent": 2,
  "type-glass": "HR++"
}

"Building", "BuildingPart", "BuildingRoom", "BuildingStorey", "BuildingUnit", and "BuildingInstallation" can have the following semantics:

For "WaterBody":

For Transportation ("Road", "Railway", "TransportSquare"):

It is possible to define and use other semantics, but these have to start with a "+", inline with the rules defined in the § 8 Extensions.

{
  "type": "+SupportingWall"
}

Because in one given City Object (say a "Building") several primitives can have the same semantics (think of a complex building that has been triangulated, there can be dozens of triangles used to model the same surface), a Semantic Object has to be declared once, and each of the primitives that are represented by it points to it. This is achieved by first declaring all the Semantic Objects in an array, and then having an array where each primitive links to Semantic Objects (position in the array).

If a Geometry object has semantics, then the Geometry object:

Also:

For legacy reasons, we use "surfaces" to name the array of Semantic Object. Nevertheless, this property is used for points and linestrings of MultiPoints and MultiLineStrings, as well.
{
  "type": "MultiSurface",
  "lod": "2",
  "boundaries": [
    [[0, 3, 2, 1]], 
    [[4, 5, 6, 7]], 
    [[0, 1, 5, 4]], 
    [[0, 2, 3, 8]], 
    [[10, 12, 23, 48]]
  ],
  "semantics": {
    "surfaces" : [
      {
        "type": "WallSurface",
        "slope": 33.4,
        "children": [2]
      }, 
      {
        "type": "RoofSurface",
        "slope": 66.6
      },
      {
        "type": "+PatioDoor",
        "parent": 0,
        "colour": "blue"
      }
    ],
    "values": [0, 0, null, 1, 2]
  }
}
{
   "type": "CompositeSolid",
   "lod": "2.2",
   "boundaries": [
     [ //-- 1st Solid
       [ [[0, 3, 2, 1, 22]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], [[1, 2, 6, 5]] ]
     ],
     [ //-- 2nd Solid
       [ [[666, 667, 668]], [[74, 75, 76]], [[880, 881, 885]] ] 
     ]    
   ],
   "semantics": {
     "surfaces" : [
       {      
         "type": "RoofSurface"
       }, 
       {
         "type": "WallSurface"
       }
     ],
     "values": [
       [ //-- 1st Solid
         [0, 1, 1, null]
       ],
       [ //-- 2nd Solid get all null values
         [null, null, 1]
       ]
     ]
   }
 }  

3.4. Geometry templates

CityGML’s "ImplicitGeometries", better known in computer graphics as templates, are one method to compress files since the geometries (such as benches, lamp posts, and trees), need only be defined once. In CityJSON, they are implemented differently from what is specified in CityGML: they are defined separately in the file, and each template can be reused. By contrast, in CityGML, the geometry used for a given City Object is reused by other City Objects, there is thus no central location where all templates are stored.

The Geometry Templates are defined as a JSON object that:

"geometry-templates": {
  "templates": [
    {
      "type": "MultiSurface",
      "lod": "2.1",
      "boundaries": [ 
         [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]]
      ]
    },
    {
      "type": "MultiSurface",
      "lod": "1.3",
      "boundaries": [ 
         [[1, 2, 6, 5]], [[2, 3, 7, 6]], [[3, 0, 4, 7]]
      ]
    }
  ],
  "vertices-templates": [
    [0.0, 0.5, 0.0],
    ...
    [1.0, 1.0, 0.0],
    [0.0, 1.0, 0.0]
  ]
}

A given template can be used as the geometry (or as one of the geometries) of a City Object. A new JSON object of type "GeometryInstance" is defined, and it:

{
  "type": "SolitaryVegetationObject", 
  "geometry": [
    {
      "type": "GeometryInstance",
      "template": 0,
      "boundaries": [372],
      "transformationMatrix": [
        2.0, 0.0, 0.0, 0.0,
        0.0, 2.0, 0.0, 0.0,
        0.0, 0.0, 2.0, 0.0,
        0.0, 0.0, 0.0, 1.0
      ]
    }
  ]
}
The CityJSON website has a page to help developers calculate coordinates for Geometry Templates and other Geometry Objects.

4. Transform Object

To reduce the size of a CityJSON object (and thus the size of files) and to ensure that only a fixed number of digits is stored for the coordinates of the geometries, the coordinates of the vertices of the geometries are represented integer values. We therefore need to store the scale factor and the translation needed to obtain the original coordinates (stored with floats/doubles).

A CityJSON object must therefore have one member "transform", whose values are 2 mandatory JSON objects ("scale" and "translate"), both arrays with 3 values.

The scheme of TopoJSON (called quantization) is reused, and here we simply add a third coordinate because our vertices are embedded in 3D space.

It should be noticed that only the "vertices" at the root of the CityJSON object are affected by the transformation, the vertices for the Geometric templates and textures are not.

To obtain the real position of a given vertex v, we must take the 3 values vi listed in the "vertices" member and:

v[0] = (vi[0] * ["transform"]["scale"][0]) + ["transform"]["translate"][0]
v[1] = (vi[1] * ["transform"]["scale"][1]) + ["transform"]["translate"][1]
v[2] = (vi[2] * ["transform"]["scale"][2]) + ["transform"]["translate"][2]

The following "transform" means that 2 important digits are kept (thus millimetre level if meters are the units of the CRS), and the "translate" usually matches with the minimum values of the axis-aligned bounding box (but does not need to).

"transform": {
    "scale": [0.001, 0.001, 0.001],
    "translate": [442464.879, 5482614.692, 310.19]
}

5. Metadata

The core of CityJSON supports the following six properties, these are compliant with the international standard ISO19115.

"metadata": {
  "geographicalExtent": [ 84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9 ],
  "identifier": "eaeceeaa-3f66-429a-b81d-bbc6140b8c1c",
  "pointOfContact": {
    "contactName": "3D geoinformation group, Delft University of Technology",
    "contactType": "organization",
    "role": "owner",
    "phone": "+31-6666666666",
    "emailAddress": "3dgeoinfo-bk@tudelft.nl",
    "website": "https://3d.bk.tudelft.nl",
    "address": "Julianalaan 134, Delft 2628BL, the Netherlands"
  },
  "referenceDate": "1977-02-28",
  "referenceSystem": "https://www.opengis.net/def/crs/EPSG/0/2355",
  "title": "Buildings in LoD2.3 of Chibougamau, Québec"
}
The storage of additional ISO19115-compliant metadata attributes and/or of statistics related to 3D city models can be done with the MetadataExtended Extension. Examples of extra attributes/properties that can be stored: point of contact for the dataset, lineage, statistics about the present LoDs, the presence of textures/materials, etc.

5.1. geographicalExtent (bbox)

While this can be extracted from the dataset itself, it is often useful to store it. It may be stored as an array with 6 values: [minx, miny, minz, maxx, maxy, maxz]. Notice that these are in the real coordinates of the dataset (based on § 5.5 referenceSystem (CRS)) and not after the coordinates have been compressed with the "transform" property (§ 4 Transform Object).

"metadata": {
  "geographicalExtent": [ 84710.1, 446846.0, -5.3, 84757.1, 446944.0, 40.9 ]
}

5.2. identifier

A unique identifier for the dataset. It is recommend to use universally unique identifier, but it is not necessary.

"metadata": {
  "identifier": "44574905-d2d2-4f40-8e96-d39e1ae45f70"
}

5.3. pointOfContact

The point of contact for the dataset. It is a JSON object that

"pointOfContact": {
  "contactName": "Justin Trudeau",
  "emailAddress": "justin.trudeau@parl.gc.ca",
  "phone": "+1-613-992-4211",
  "address": "24 Sussex Drive, Ottawa, Canada",
  "contactType": "individual",
  "role": "pointOfContact"
}

5.4. referenceDate

The date where the dataset was compiled, without the time of the day, only a "full-date" in RFC 3339, Section 5.6 should be used.

"metadata": {
  "referenceDate": "1977-02-28"
}
JSON does not have a date type, and thus the representations defined by RFC 3339, Section 5.6 should be used. A simple date is "full-date" (thus "1977-07-11" as a string), and should be used for the metadata above.

Other attributes in a CityJSON object can also have a date with a time, and such an attribute is specified as a "full-time". For example "1985-04-12T23:20:50.52Z" (stored as a string).

5.5. referenceSystem (CRS)

The coordinate reference system (CRS) may be given as a URL, formatted this way according to the OGC Name Type Specification:

http://www.opengis.net/def/crs/{authority}/{version}/{code}

where {authority} designates the authority responsible for the definition of this CRS (usually "EPSG" or "OGC"), and where {version} designates the specific version of the CRS ("0" (zero) is used if there is no version).

For instance, for the Dutch national CRS in 3D:

"metadata": {
  "referenceSystem": "https://www.opengis.net/def/crs/EPSG/0/7415"
}

Be aware that the CRS should be a three-dimensional one, ie the elevation/height values should be with respect to a specific datum.

Unlike in (City)GML where each object can have a different CRS (eg a wall of a building could theoretically have a different from the other walls used to represent the building), in CityJSON all the city objects need to be in the same CRS.

5.6. title

A string describing the dataset.

"metadata": {
  "title": "3D city model of Chibougamau, Canada"
}

6. Appearance Object

Both textures and materials are supported in CityJSON, and the same mechanisms used in CityGML are reused, so the conversion back-and-forth is easy. The material is represented with the X3D specifications, as is the case for CityGML. For the texture, the COLLADA standard is reused, as is the case for CityGML. However:

An Appearance Object is a JSON object that

"appearance": {
  "materials": [],
  "textures":[],
  "vertices-texture": [],
  "default-theme-texture": "myDefaultTheme1",
  "default-theme-material": "myDefaultTheme2"
}

6.1. Geometry Object having material(s)

Each surface in a Geometry Object can have one or more materials assigned to it. To store the material of a surface, a Geometry Object may have a member "material", the value of this member is a collection of key-value pairs, where the key is the theme of the material, and the value is one JSON object that must contain either:

In the following, the Solid has 4 surfaces, and there are 2 themes ("irradiation" and "irradiation-2"). These could represent, for instance, the different colours based on different scenarios of an solar irradiation analysis. Notice that the last surface gets no material (for both themes), thus null is used.

{
  "type": "Solid",
  "lod": "2.1",
  "boundaries": [
    [ [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], [[1, 2, 6, 5]] ] 
  ],
  "material": {
    "irradiation": { 
      "values": [[0, 0, 1, null]] 
    },
    "irradiation-2": { 
      "values": [[2, 2, 1, null]] 
    }
  }
}

6.2. Geometry Object having texture(s)

To store the texture(s) of a surface, a Geometry Object may have a member with the value "texture", its value is a collection of key-value pairs, where the key is the theme of the textures, and the value is one JSON object that must contain one member "values", whose value is a hierarchy of arrays with integers. For each ring of each surface, the first value refers to the position (0-based) in the "textures" member of the "appearance" member of the CityJSON object. The other indices refer to the UV positions of the corresponding vertices (as listed in the "boundaries" member of the geometry). Each array representing a ring therefore has one more value than that to store its vertices.

The depth of the array depends on the Geometry object, and is equal to the depth of the "boundary" array.

In the following, the Solid has 4 surfaces, and there are 2 themes: "winter-textures" and "summer-textures" could for instance represent the textures during winter and summer.. Notice that the last 2 surfaces of the first theme gets no material, thus null is used.

{
  "type": "Solid",
  "lod": "2.2",
  "boundaries": [
    [ [[0, 3, 2, 1]], [[4, 5, 6, 7]], [[0, 1, 5, 4]], [[1, 2, 6, 5]] ] 
  ],
  "texture": {
    "winter-textures": {
      "values": [
        [ [[0, 10, 23, 22, 21]], [[0, 1, 2, 6, 5]], [[null]], [[null]] ]                  
      ]
    },
    "summer-textures": {
      "values": [
        [ 
          [[1, 10, 23, 22, 21]], 
          [[1, 1, 2, 6, 5]], 
          [[1, 66, 12, 64, 5]], 
          [[2, 99, 21, 16, 25]] 
        ]                  
      ]      
    }
  }     
}        

6.3. Material Object

A Material Object:

If only "name" is defined for the Material Object, then it is up to the application that reads the CityJSON file to attach a material definition to the "name". This might not always be possible. Therefore, it is advised to define as many from the optional members as needed for fully displaying the material.
"materials": [
  {
    "name": "roofandground",
    "ambientIntensity":  0.2000,
    "diffuseColor":  [0.9000, 0.1000, 0.7500],
    "emissiveColor": [0.9000, 0.1000, 0.7500],
    "specularColor": [0.9000, 0.1000, 0.7500],
    "shininess": 0.2,
    "transparency": 0.5,
    "isSmooth": false
  },
  {
    "name": "wall",
    "ambientIntensity":  0.4000,
    "diffuseColor":  [0.1000, 0.1000, 0.9000],
    "emissiveColor": [0.1000, 0.1000, 0.9000],
    "specularColor": [0.9000, 0.1000, 0.7500],
    "shininess": 0.0,
    "transparency": 0.5,
    "isSmooth": true
  }            
]

6.4. Texture Object

A Texture Object:

"textures": [
  {
    "type": "PNG",
    "image": "http://www.someurl.org/filename.jpg"
  },
  {
    "type": "JPG",
    "image": "appearances/myroof.jpg",
    "wrapMode": "wrap",
    "textureType": "unknown",
    "borderColor": [0.0, 0.1, 0.2, 1.0]
  }      
]

6.5. Vertices-texture Object

An Appearance Object may have one member named "vertices-texture", whose value is an array of the (u,v) coordinates of the vertices used for texturing surfaces. Their position in this array (0-based) is used by the "texture" member of the Geometry Objects.

"vertices-texture": [
  [0.0, 0.5],
  [1.0, 0.0],
  [1.0, 1.0],
  [0.0, 1.0]
]

7. Handling large files

Because CityJSON aims at being easy-to-use and developers friendly, it is advised to keep the size of CityJSON files small. Files of several hundreds of megabytes are bad practice, and should be avoided since users will have great difficulties visualising and manipulating them.

7.1. Decomposing an area into parts/tiles

One solution to handle a large dataset is to subdivide it into tiles or regions, and ensure that each part has a reasonable size. Each part becomes a CityJSON file.

7.2. Text sequences and streaming with CityJSONFeature

Another solution is to decompose a CityJSON object into its features (the City Objects), create several JSON objects, and store them in a JSON Lines text (also called Newline Delimited JSON). This is a format to store several JSON objects in a single file, and allows the processing of each object one at a time.

A CityJSON Feature Object allows us to store one feature, for instance a "Building" with eventually its children "BuildingPart" and/or "BuildingInstallation". Unlike a CityJSON Object, all the vertices and appearances of the object are local.

A CityJSON Feature Object:

{
  "type": "CityJSONFeature",
  "id": "myid", 
  "CityObjects": {},
  "vertices": [],
  "appearance": {}
}
{
  "type": "CityJSONFeature",
  "id": "id-1", 
  "CityObjects": {
    "id-1": {
      "type": "Building", 
      "attributes": { 
        "roofType": "gabled roof"
      },
      "children": ["mybalcony"],
      "geometry": [...]
    },
    "mybalcony": {
      "type": "BuildingInstallation", 
      "parents": ["id-1"],
      "geometry": [...]
    }
  },
  "vertices": [...]
}

The following root property of a CityJSON Object are not allowed in a CityJSONFeature Object:

These properties may be accessible, for instance as a JSON object (the first one) in a JSON Lines text stream, as in the following example:

{"type": "CityJSON","version": "1.1","transform":{...},"metadata":{...}}
{"type": "CityJSONFeature", "id": "a", "CityObjects":{...},"vertices":[...]} 
{"type": "CityJSONFeature", "id": "b", "CityObjects":{...},"vertices":[...]} 
{"type": "CityJSONFeature", "id": "c", "CityObjects":{...},"vertices":[...]} 

However, it should be noticed that CityJSON does not prescribe the format or standard that should be used to store several JSON objects in a given file, it only defines how "CityJSON" and "CityJSONFeature" objects should be defined.

8. Extensions

CityJSON uses JSON Schemas to document and validate the data model, schemas are one way to validate the syntax of a JSON document.

A CityJSON Extension is a JSON file that allows us to document how the core data model of CityJSON may be extended, and to validate CityJSON files. This is conceptually akin to the Application Domain Extensions (ADEs) in CityGML.

A CityJSON Extension can extend the core data model by three ways:

  1. Adding new complex attributes to the City Objects of the core

  2. Adding new properties at the root of a document

  3. Creating a new City Object, or "extending" one, and defining complex geometries

While Extensions are less flexible than CityGML ADEs (inheritance and namespaces are for instance not supported, and less customisation is possible), it should be noted that the flexibility of ADEs comes at a price: the software processing an extended CityGML file will not necessarily know what structure to expect.

There is ongoing work to use the ADE schemas to automatically do this, but this currently is not supported by most software. Viewers might not be affected by ADEs because the geometries are usually not changed by an ADE (although they can!). However, software parsing the XML to extract attributes and features might not work directly (and thus specific code would need to be written).

CityJSON Extensions are designed such that they can be read and processed by standard CityJSON software, often no changes in the parsing code is required. This is achieved by enforcing a set of 6 simple rules (see § 8.6 Rules to follow to define new City Objects) when adding new City Objects. If these are followed, then a CityJSON file containing Extensions will be seen as a "standard" CityJSON file.

8.1. Using an Extension in a CityJSON file

An Extension should be given a name (eg "Noise") and the URL of the Extension file should be given, along with the version that is used for this file. It is expected that the Extension is publicly available at the URL, and can be downloaded.

Several Extensions can be used in a single CityJSON Object, each one is indexed by its name in the "extensions" JSON object. In the example below we have two Extensions: one named "Noise" and one named "Solar_Potential".

{
  "type": "CityJSON",
  "version": "1.1",
  "extensions": {
    "Noise": {
      "url" : "https://someurl.org/noise.json",
      "version": "2.0"
    },
    "Solar_Potential": {
      "url" : "https://someurl.org/solar.json",
      "version": "0.8"
    }
  },
  "CityObjects": {},
  "vertices": []
}

8.2. The Extension file

A CityJSON Extension is a JSON object, and it must have the following 8 members:

  1. one member with the name "type", whose value must be "CityJSONExtension";

  2. one member with the name "name", whose value must be a string identifying the extension;

  3. one member with the name "uri", whose value must be a string with the URI of the location of the schema where the JSON object is located;

  4. one member with the name "version", whose value must be a string identifying the version of the Extension;

  5. one member with the name "versionCityJSON", whose value must be a string (X.Y) identifying the version of CityJSON that uses the Extension;

  6. one member with the name "extraRootProperties", whose value must be a JSON object; its content is part of a JSON schema (explained below), or an empty object;

  7. one member with the name "extraAttributes", whose value must be a JSON object; its content is part of a JSON schema (explained below), or an empty object;

  8. one member with the name "extraCityObjects", whose value must be a JSON object; its content is part of a JSON schema (explained below), or an empty object;

{
  "type": "CityJSONExtension",
  "name": "Noise",
  "description": "Extension to model the noise",
  "uri": "https://someurl.org/noise.ext.json",
  "version": "0.5",
  "versionCityJSON": "1.1",
  "extraRootProperties": {},     
  "extraAttributes": {},
  "extraCityObjects": {}
}
If an element of the Extension reuses, or references, structures and/or objects defined in the schemas of CityJSON, then we can assume that the Extension is in the same folder as the schemas. An example would be to reuse the Solid type:
"items": {
  "oneOf": [
    {"$ref": "geomprimitives.json#/Solid"}
  ]
}

8.3. Case 1: Adding new complex attributes to existing City Objects

One of the philosophies of JSON is "schema-less", which means that one is allowed to define new properties for the JSON objects without documenting them in a JSON schema (watch out: this does not mean that JSON does not have schemas!). While this is in contrast to CityGML (and GML as a whole) where the schemas are central, the schemas of CityJSON are (partly) following that philosophy.

If one wants to document the colour of a given "Building" ("colour": "red"), the easiest way is just to add a new property to the City Object attributes:

{
  "type": "Building", 
  "attributes": { 
    "storeysAboveGround": 2,
    "colour": "red"
  },
  "geometry": [...]
}

It is also possible to add, and document in a schema, complex attributes, for example if we wanted to have the colour of the buildings as a RGBA value (red-green-blue-alpha):

{
  "type": "Building", 
  "attributes": { 
    "storeysAboveGround": 2,
    "+colour": {
      "rgba": [255, 255, 255, 1]
    }
  },
  "geometry": [...]
}

Another example would be to store the area of the parcel of a building, and also to document the unit of measurement (UoM):

{
  "type": "Building", 
  "attributes": { 
    "storeysAboveGround": 2,
    "+area-parcel": {
      "value": 437,
      "uom": "m2"
    } 
  },
  "geometry": [...]
}

For these two cases, the CityJSON Extension object would look like the snippet below. Notice that "extraAttributes" may have several properties (the types of the City Objects are the possibilities) and then each of these has as properties the new attributes (there can be several).

An extra attribute must start with a "+"; it is good practice to prepend the attribute with the name of the Extension, to avoid that 2 attributes from 2 different extensions have the same name.

The value of the property is a JSON schema, this schema can reference and reuse JSON objects already defined in the CityJSON schemas.

"extraAttributes": {
  "Building": {
    "+colour": {
      "type": "object",
      "properties": {
        "rgba": {
          "type": "array",
          "items": {"type": "number"},
          "minItems": 4,    
          "maxItems": 4
        }
      },
      "required": ["rgba"],
      "additionalProperties": false
    },
    "+area-parcel": {
      "type": "object",
      "properties": {
        "value": { "type": "number" },
        "uom": { "type": "string", "enum": ["m2", "feet2"] }
      },
      "required": ["value", "uom"],
      "additionalProperties": false
    }      
  } 
}

8.4. Case 2: Adding new properties at the root of a document

It is allowed to add a new property at the root of a CityJSON file, but if one wants to document it in a schema, then this property must start with a "+". Imagine we wanted to store some census data for a given neighbourhood for which we have a CityJSON file, then we could define the extra root property "+census" as follows:

"extraRootProperties": {
  "+census": {
    "type": "object",
    "properties": {
      "percent_men": { 
        "type": "number",
        "minimum": 0.0,
        "maximum": 100.0
      },
      "percent_women": { 
        "type": "number",
        "minimum": 0.0,
        "maximum": 100.0
      }
    }
  }
}

And a CityJSON file would look like this:

{
  "type": "CityJSON",
  "version": "1.1",
  "extensions": {
    "Census": {
      "url": "https://someurl.org/census.ext.json",
      "version": "0.7"
    }
  },
  "CityObjects": {...},
  "vertices": [...],
  "+census": {
    "percent_men": 49.5,
    "percent_women": 51.5
  }
}

8.5. Case 3: Creating and/or extending new City Objects

The creation of a new City Object is done by defining it in the CityJSON Extension object in the "extraCityObjects" property:

"extraCityObjects": {
  "+NoiseBuilding": {
    "allOf": [
      { "$ref": "cityobjects.json#/_AbstractBuilding" },
      {
        "properties": {
          "type": { "enum": ["+NoiseBuilding"] },
          "attributes": {
            "properties": {
              "buildingLDenMin": {"type": "number"}
            }
          }
        },
        "required": ["type"]
      }
    ]
  }
}
"extraCityObjects": {
  "+NoiseBuildingPart": {
    "allOf": [
      { "$ref": "cityobjects.json#/_AbstractBuilding" },
      {
        "properties": {
          "type": { "enum": ["+NoiseBuildingPart"] },
          "attributes": {
            "properties": {
              "buildingLDenMin": {"type": "number"}
            }
          }
        },
        "required": ["type", "parents"]
      }
    ]
  }
}

Since all City Objects are documented in the schemas of CityJSON (in cityobjects.schema.json), it is basically a matter of copying the parts needed in a new file and modifying its content.

A new name for the City Object must be given and it must begin with a "+".

Because City Objects can be of different levels (1st-level ones can exist by themselves; 2nd-level ones need to have a parent), we need to explicitly define that "parents" is mandatory for 2nd-level objects.

Please note that since JSON schemas do not allow inheritance, the only way to extend a City Object is to define an entirely new one (with a new name, eg "+NoiseBuilding"). This is done by copying the schema of the parent City Object and extending it.

8.6. Rules to follow to define new City Objects

The challenge when creating Extensions to the core model is that we do not want to break the software packages (viewers, spatial analysis, etc) that already read and process CityJSON files. While one could define a new City Object and document it, if this new object does not follow the rules below then it will mean that new specific software needs to be built for it---this would go against the fundamental ideas behind CityJSON.

  1. The name of a new City Object must begin with a "+", eg "+NoiseBuilding".

  2. A new City Object must conform to the rules of CityJSON, ie it must contain a property "type". If the object contains appearances, the same mechanism should be used so that the new City Objects can be processed without modification.

  3. All the geometries must be in the property "geometry", and cannot be located somewhere else deep in a hierarchy of a new property. This ensures that all the code written to process, manipulate, and view CityJSON files will be working without modifications.

  4. If a new City Object contains other objects and requires different geometries, then a new City Object needs to be defined using the parents-children structure of CityJSON, as used by "Building" and "BuildingPart".

  5. The reuse of types defined in CityJSON, eg "Solid" or semantic surfaces, is allowed.

  6. To define a new semantic surface (besides the ones prescribed, see § 3.3 Semantics of geometric primitives), a "+" must be prepended to its name, eg "+ThermalSurface".

9. CityJSON Schemas

The JSON schemas of the specifications are publicly available at https://cityjson.org/schemas/.

10. CityGML v3.0 conformance

CityJSON v1.1 is conformant with the CityGML v3.0 data model, although not all extension modules have been implemented.

The details of which modules are supported (and where the so-called null mapping are applied, see CityGML Modularization), are available at https://www.cityjson.org/conformance/v30/