Custom Data Properties
An annotation's geometry and class don't tell you everything you might need to know about it. A tree's species, a building's inspection status, a pipeline segment's material, or a piece of equipment's serial number — none of that can be inferred from a polygon. Custom data properties let a project track exactly the extra facts it cares about, on every annotation, without any custom development.
A few examples of what teams use this for:
- A forestry project recording
species,height_m, andlast_inspectedon tree crowns. - A building-inspection workflow flagging
is_condemned, or rating each structure'scondition(poor / fair / good / excellent). - A pipeline network tracking
material,install_date, andpressure_rating_psiper segment. - An asset-management project storing a
serial_number, or a running list ofmaintenance_dates, per piece of equipment.
This is different from the properties covered in Properties: class, confidence, and geometry-derived metrics are automatic and identical across every project. Custom data properties are whatever your project defines them to be.
How it works
A project owner defines the shape of this data once — as a schema — in the project's settings. From then on, every annotation in that project can carry a value for each field in that schema.
Viewing and editing in the Inspector
Select one or more annotations in the workspace to view and edit their custom data properties in the Inspector panel. Exactly how each field is displayed, and how it behaves, depends entirely on how it's described in the project's schema.
If you select several annotations at once and their values for a field disagree, the control shows a "Mixed" state instead of any single value; changing it applies the new value to every selected annotation.
Exporting to GeoJSON
Custom data properties can be included when exporting annotations as GeoJSON. Open the export dialog and look for the Data Properties section: each field defined in the schema (or the entire value, if the schema isn't a plain object) appears there with its own include toggle and editable output column name, exactly like the built-in properties above it. See Export Vector Layers for the full export flow.
Importing from GeoJSON
Custom data properties can also be set when importing annotations from a GeoJSON file. In the import dialog's Data Properties section, each field defined in the schema (or the entire value, if the schema isn't a plain object) gets its own row with two controls:
- Column — which property key in the uploaded GeoJSON file to read the value from for each feature, or Use Default to always use the default value below instead.
- Default — the value to use when a feature doesn't have that column, or when no column is selected at all. This is shown using the same type-aware control described in the table below (a checkbox, a color picker, a nested object breakdown, and so on). It's left blank by default — importing without touching it simply leaves that property unset on annotations missing the column, rather than filling in a guessed value.
If a column in the uploaded file happens to share its name with a schema
field (or is named property for a non-object schema), it's selected
automatically — this is what makes import and export round-trip cleanly: a
file exported from a project with its default column names, re-imported
into that same project with no changes, produces the same data property
values you started with.
Data Schema
The schema itself is a standard JSON Schema document — the same open specification used across the industry to describe JSON data. Anything you can express in JSON Schema (types, required fields, enums, nested objects and arrays, and more) is supported, and unfamiliar readers should start with the JSON Schema getting-started guide before writing or editing one.
A field's title and description aren't just documentation — they're
shown to your users too: title becomes the field's label in the Inspector
(falling back to its property name if omitted), and description becomes a
hint users can hover to read. Some validation keywords also directly affect
how a field can be interacted with, not just whether it's considered valid —
for example, a string's maxLength is enforced right in the text input, and
a number's minimum/maximum constrain its input control, not just
after-the-fact validation.
A few keywords are given special treatment so they render as a friendlier control than a plain text box:
| If a field has... | It's shown as | For example |
|---|---|---|
type: "boolean" | A checkbox | is_destroyed, is_grounded |
type: "number" or "integer" | A number input | altitude_m, tree_ring_count |
type: "string" | A text field | species, inspection_notes |
type: "string", format: "color" | A color picker | marker_color |
type: "string", format: "date" or "date-time" | A date or date-and-time picker | last_inspected |
type: "string", format: "url" or "uri" | A clickable link (still validated and filtered as plain text) | manufacturer_page |
const, enum, or a union of enum values | A single-select dropdown | condition (poor / fair / good / excellent) |
anyOf / oneOf with differing shapes | A type selector, plus the selected option's own fields | shape (circle / square / triangle, each with different fields) |
type: "object" with properties | A collapsible group of fields | material_composition (a breakdown of sub-fields) |
type: "array" with items | A collapsible, addable list | inspection_dates |
| None of the above | A raw JSON editor | Anything not covered above — no schema is a dead end |
Fields with more than one possible shape
anyOf and oneOf let a field be one of several different shapes — for
example, a shape field that's either a circle (with a radius), a square
(with a side), or a triangle (with a base and height). When the
options genuinely differ, the Inspector shows a type selector so you can
choose which one applies, with only that option's own fields shown
underneath. In the Attribute Table the column shows each annotation's own
value in whichever shape it matches, but can no longer be sorted or
filtered — any filter previously saved on that column is dropped.
Each option's label in that selector is chosen automatically, in this
order: a shared discriminator property you've named explicitly, a
constant value shared by every option (even without naming one), each
option's own title, or otherwise a plain "Option 1", "Option 2", and so
on. Whichever property ends up identifying the option is not shown again
among its own fields, since the selector already displays it.
To name the discriminator explicitly, add a discriminator.propertyName
alongside oneOf/anyOf, and give every option a matching const for
that property:
{
"discriminator": { "propertyName": "kind" },
"oneOf": [
{
"type": "object",
"title": "Circle",
"properties": {
"kind": { "const": "circle" },
"radius": { "type": "number", "title": "Radius" }
}
},
{
"type": "object",
"title": "Square",
"properties": {
"kind": { "const": "square" },
"side": { "type": "number", "title": "Side" }
}
}
]
}
Every option must declare a const for that exact property, or it won't
count toward identifying that option. Leaving out discriminator entirely
still works, as long as every option shares one property with a const
value that's actually different from option to option — a property
that's present everywhere but has the same value on every option (a
version tag, for example) doesn't count, since it couldn't tell two
options apart anyway, and is skipped in favor of one that can. Naming the
discriminator explicitly skips that check — it's trusted as declared — but
still give every option its own distinct value: two options sharing one
value can't be told apart from the stored data, so the later one is shown
greyed out in the selector and can never be chosen, and its annotations
render under the first option's fields instead.
This only works reliably when the options can actually be told apart — either by that shared constant, or because they're different underlying types (a string vs. a number, for example). Two options that are both plain strings (or both plain numbers) with nothing distinguishing them will always show as the first option, since there's nothing in the stored value itself to tell them apart.
allOf is different: it combines every option's fields into one, rather
than offering a choice between them. Combining allOf with oneOf/anyOf
on the same field — a shared set of base fields alongside the variant
choice — keeps both: every option gets the base fields in addition to its
own.
Editing the schema
Project owners can edit a project's schema from Project Settings → Project Schema, either visually or as raw JSON:
- Visual (the default) is a form-based editor: add, rename, or remove fields; pick each field's kind from the same set covered in the table above; and configure nested objects, arrays (homogeneous items only — a tuple-form array falls back to Raw JSON), and multi-shape (union) fields, without writing JSON by hand.
- Raw JSON edits the underlying JSON Schema document directly — needed
for anything the visual editor doesn't have a dedicated control for yet,
such as
allOfor a$ref.
Switching between the two never loses data: anything the visual editor can't represent is left untouched, shown there as read-only with a prompt to switch to Raw JSON to edit it.
Editing the schema, in either mode, changes how every annotation in the project is displayed and edited, immediately, for every user of that project. Renaming or removing a field doesn't delete the values already stored under its old name on existing annotations — they're simply no longer shown or editable until the schema is changed back — but changing a field's type can make existing values fall back to the raw JSON editor if they no longer match. Know what you're changing before you save: if you're using Raw JSON and aren't comfortable authoring JSON Schema by hand, it's worth reviewing the specification first.