Mapping Configuration Guide

The Cassiopeia mapping configuration system transforms data from various sources (JSON, CSV, GeoJSON, GBFS, GTFS) into NGSI-LD compliant entities. You define configuration files that specify how fields from source data map to target attributes.

Configuration File Format

Supported Formats

The system supports both JSON5 and JSON. JSON5 is recommended for ease of use. JSON5 allows:

  • Comments

  • Trailing commas

  • Unquoted keys

  • Additional syntax conveniences

See the JSON5 specification for details.

Basic Structure

{
    version: "v3",
    dataModel: "EntityTypeName",
    identity: {
        entityName: "{{ source_field }}",
        urn: {
            site: "optional-site",
            service: "optional-service",
            group: "optional-group"
        }
    },
    attributes: {
        // Attribute definitions
    }
}

Required Fields

  • version — Must be "v3" (only version 3 is supported)

  • dataModel — String identifier for the target entity type

  • identity — Configuration for generating entity IDs

  • attributes — Object containing attribute definitions

Templates and Data Mapping

How Templates Work

Templates use double curly braces {{ }} to extract data from source files. The text inside the braces refers to field names in your source data.

Example: For JSON data:

{
    "station_id": "ST123",
    "name": "Central Station",
    "lat": 45.4642,
    "lon": 9.1900
}

In your mapping configuration:

  • {{ station_id }} → extracts "ST123"

  • {{ name }} → extracts "Central Station"

  • {{ lat }} → extracts 45.4642

  • {{ lon }} → extracts 9.1900

Finding Field Names

To determine which field names to use in templates:

  1. Inspect your source data — Open the JSON, CSV, or GeoJSON file and examine its structure

  2. Use exact field names — Match the names from your data

  3. Use dot notation for nested data — For example, {{ properties.station_name }}

CSV: If headers are STATION_ID,NAME,LATITUDE,LONGITUDE, use {{ STATION_ID }}, {{ NAME }}, and so on.

CSV with spaces or special characters: For headers like "Località di intervento" or "Tot posti", use bracket notation:

  • {{ this['Località di intervento'] }}

  • {{ this['Tot posti'] }}

GeoJSON: For a structure with properties and geometry:

  • {{ properties.BIKE_SH }} — bike station name

  • {{ properties.STALLI }} — number of stalls

  • {{ geometry }} — geometry data

Template Examples

// Simple field extraction
"{{ station_id }}"

// String concatenation
"Station-{{ id }}"

// Multiple fields
"{{ street }} {{ number }}, {{ city }}"

// Default values
"{{ bike_id | default(value=vehicle_id) }}"

Advanced Template Usage

The mapping system uses the Tera templating engine for complex transformations.

Conditional Logic

category: {
    source: "{% if properties.tipo == 'RESIDENTI/PUBBLICI' or properties.tipo == 'PUBBLICI/RESIDENTI' %}publicPrivate{% elif properties.tipo == 'PUBBLICI' %}public{% elif properties.tipo == 'AUTORIMESSA CONVENZIONATA' %}onlyWithPermit{% endif %}",
    type: "Property",
    transformation: "array",
}

Array Sources

occupancyDetectionType: {
    source: ["none", "manual"],
    type: "Property",
    transformation: "array",
}

String and Number Operations

"{{ name | upper }}"
"{{ name | lower }}"
"{{ name | capitalize }}"
"{{ description | truncate(length=50) }}"
"{{ date | dateformat('%Y-%m-%d') }}"
"{{ price * 1.2 }}"
"{{ count + offset }}"

Context Variable

For list relationships, {{ context }} refers to each item:

properties: {
    count: {
        source: "{{ context }}",
        type: "Property",
        transformation: "integer",
    }
}

Template Syntax Reference

The system uses the Tera templating engine. Common features:

Variable access: * {{ variable }} — Print a variable * {{ object.property }} — Access object properties * {{ array.0 }} — Access array elements * {{ object['key'] }} — Access properties with dynamic keys

Conditionals: * {% if condition %} …​ {% endif %} * {% if condition %} …​ {% else %} …​ {% endif %} * {% if condition %} …​ {% elif condition %} …​ {% endif %}

Filters: * {{ value | filter_name }} * {{ value | filter_name(param="value") }} * Common filters: upper, lower, capitalize, trim, default, length

Operators: * Comparison: ==, !=, >, <, >=, * Logical: and, or, not * Arithmetic: +, -, *, /, %

Data Model Configuration

{
    dataModel: "BikeHireDockingStation"
}

The dataModel field specifies which NGSI-LD data model your entities follow. It serves two purposes:

  1. Validation — If a matching schema exists, output is validated against it

  2. Type definition — Defines the entity type for downstream systems

Use FIWARE Smart Data Models when possible (for example, BikeHireDockingStation, WeatherStation, TrafficFlowObserved). Custom strings such as MyCustomSensorType are also supported.

Identity Configuration

The identity section defines how entities are uniquely identified:

identity: {
    entityName: "{{ station_id }}",
    urn: {
        site: "milano",
        service: "mobility",
        group: "bikeshare"
    }
}

Entity Name

  • entityName — A template that creates the base identifier for each entity

  • Does not need to be unique — the system appends incrementing numbers for duplicates (for example, sensor-1, sensor-2)

Examples:

entityName: "{{ station_id }}"
entityName: "Station-{{ id }}"
entityName: "sensor-{{ device_id }}"
entityName: "static-sensor"  // Becomes "static-sensor-1", "static-sensor-2", etc.

URN Components

The optional urn section adds hierarchical structure:

  • site — Physical location or site identifier (for example, milano, london)

  • service — Service category or domain (for example, mobility, environment, traffic)

  • group — Specific group within the service (for example, bikeshare, parking, weather)

Use URN components to separate different entity groups, especially when multiple datasets share the same entity types.

Valid URN Combinations

Supported combinations (in order):

  1. No components: dataModel:entityId

  2. Site only: dataModel:site:entityId

  3. Site + Service: dataModel:site:service:entityId

  4. Site + Service + Group: dataModel:site:service:group:entityId

Invalid combinations (service without site, group without site and service, or non-sequential parts) will cause errors.

URN Output Examples

With entityName: "{{ station_id }}":

  • Without URN: urn:ngsi-ld:BikeHireDockingStation:ST123

  • Site only: urn:ngsi-ld:BikeHireDockingStation:milano:ST123

  • Site + Service: urn:ngsi-ld:BikeHireDockingStation:milano:mobility:ST123

  • All parts: urn:ngsi-ld:BikeHireDockingStation:milano:mobility:bikeshare:ST123

Attribute Types

Each attribute defines how source data is transformed into an NGSI-LD attribute.

Attribute Structure

attributeName: {
    source: "template or value",
    type: "AttributeType",
    transformation: "DataType",
    target: { ... },
    properties: { ... },
    mappings: { ... }
}
  • source — How to extract data (required)

  • type — NGSI-LD attribute type (required)

  • transformation — Data type conversion (optional)

  • target — For relationships (optional)

  • properties — For relationships or nested attributes (optional)

  • mappings — For complex object attributes (optional)

Core Attribute Types

Property

Simple key-value properties:

name: {
    source: "{{ station_name }}",
    type: "Property",
    transformation: "string"
}
capacity: {
    source: "{{ max_bikes }}",
    type: "Property",
    transformation: "integer"
}

Relationship

Links to a single other entity:

regionId: {
    source: "{{ region_id }}",
    type: "Relationship",
    target: { entity: "GbfsRegion" }
}
operator: {
    source: "{{ operator_code }}",
    type: "Relationship",
    target: {
        entity: "TransportOperator",
        idPattern: "operator-{{ operator_code }}"
    }
}

ListRelationship

Arrays of relationships with optional metadata:

// Dictionary input: { "dott_scooter": 10, "lime_bike": 5 }
vehicleCapacity: {
    source: "{{ vehicle_capacity }}",
    type: "ListRelationship",
    target: { entity: "GbfsVehicleType" },
    properties: {
        count: {
            source: "{{ context }}",
            type: "Property",
            transformation: "integer"
        }
    }
}

// Array input: [{ "vehicle_type_id": "scooter", "count": 10 }]
vehicleTypesAvailable: {
    source: "{{ vehicle_types_available }}",
    type: "ListRelationship",
    target: {
        entity: "GbfsVehicleType",
        sourceField: "vehicle_type_id"
    },
    properties: {
        count: {
            source: "{{ count }}",
            type: "Property",
            transformation: "integer"
        }
    }
}

GeoProperty

Geospatial data:

location: {
    source: ["{{ lat }}", "{{ lon }}"],
    type: "GeoProperty",
    transformation: "point"
}
geometry: {
    source: "{{ geometry }}",
    type: "GeoProperty",
    transformation: "geometry"
}

Synthetic Entities

Synthetic entities create related entities on-the-fly when they do not exist in source data. Use them for reference data such as organizations or operators.

owner: {
    source: "Comune di Milano",
    type: "ListRelationship",
    targetObjectType: "Organization",
    syntheticEntity: {
        dataModel: "Organization",
        identity: { entityName: "ComuneDiMilano" },
        attributes: {
            name: {
                source: "Comune di Milano",
                type: "Property",
                transformation: "string"
            },
            contactEmail: {
                source: "info@comune.milano.it",
                type: "Property",
                transformation: "string"
            }
        }
    }
}

Inside syntheticEntity, {{ this }} refers to the current entity. Synthetic entities are created once per unique identifier.

Accessing Fields with Special Characters

Use this['field_name'] when field names contain spaces or special characters:

name: {
    source: "Rastrelliere - {{ this['LOCALITA DI INTERVENTO'] | default(value='Unknown') }}",
    type: "Property",
    transformation: "string"
}
totalSlotNumber: {
    source: "{{ this['Tot posti'] }}",
    type: "Property",
    transformation: "integer"
}

Complex Objects with Mappings

Use mappings to create nested objects from flat source data:

address: {
    type: "Property",
    transformation: "object",
    mappings: {
        streetAddress: {
            source: "{{ street_name }} {{ street_number }}",
            type: "Property",
            transformation: "string"
        },
        addressLocality: {
            source: "{{ city }}",
            type: "Property",
            transformation: "string"
        },
        addressCountry: {
            source: "IT",
            type: "Property",
            transformation: "string"
        }
    }
}

Transformations

Primitive types: string, integer, float, boolean, array, object

Specialized types: datetime, date, time

Geometry types: point, multipoint, linestring, polygon, multilinestring, multipolygon

Mapping Requirements by Data Format

Custom formats (require user-created mappings): CSV, JSON, GeoJSON

Standard formats (pre-made mappings available): GBFS, GTFS

Pre-made mappings exist for common GBFS and GTFS entity types. For custom structures or additional fields, copy and modify these mappings.

Best Practices

  • Use simple templates when possible for better performance

  • Specify appropriate transformation for type safety

  • Use nested mappings for complex objects

  • Apply default values for optional fields: {{ field | default(value="unknown") }}

  • Use URN components consistently across related entities