EventType V2

Overview

EventType V2 represents a complete redesign of the eventtype system, addressing fundamental limitations in V1 while providing a modern, standards-based approach to defining event data collection schemas. V2 uses JSON Schema 2020-12 with reference resolution and a comprehensive UI definition system.

Key Improvements Over V1

Schema Validation & Standards

  • Static validation: Schemas can be validated without database access or template rendering

  • JSON Schema 2020-12: Uses the latest JSON Schema standard with full tooling support

  • Reference resolution: Standard $ref system replaces custom template variables

  • Type safety: Strong typing with comprehensive field type support

Performance & Reliability

  • No runtime rendering: Eliminates Jinja2 template processing overhead

  • Predictable behavior: Consistent schema resolution without template dependencies

  • Better caching: Static schemas enable effective caching strategies

  • Error handling: Comprehensive validation and error reporting

Developer Experience

  • Tool compatibility: Works with standard JSON Schema validators and editors

  • Better debugging: Clear error messages and validation feedback

  • Version control: Clean diffs without template variables

  • Documentation: Self-documenting schemas with proper metadata

UI System

  • Standardized format: Consistent UI definition structure

  • Rich field types: Support for text, numeric, choice, attachment, and collection fields

  • Responsive design: Built-in support for responsive layouts

  • Accessibility: Better accessibility support through standardized definitions

Architecture

V2 eventtypes use a dual-structure approach with separate JSON schema and UI definition sections, similar to V1 but with significant improvements in implementation and standards compliance.

Core Design Principles

1. Standards-Based Schema Definition

  • JSON Schema 2020-12: Uses the latest JSON Schema standard for maximum compatibility

  • Reference Resolution: Implements JSON Schema References for dynamic content

  • Static Validation: Schemas can be validated without database access or runtime processing

2. Reference-Based Choice Resolution

V2 replaces V1’s template variables with standard JSON Schema $ref references:

V1 Template Approach:

{
  "enum": {{enum___carcassrep_species___values}},
  "enumNames": {{enum___carcassrep_species___names}}
}

V2 Reference Approach:

{
  "anyOf": [
    {
      "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_species"
    }
  ]
}

When the reference is resolved (for example in a pre-rendered event type schema), dynamic schema endpoints expand to an enum list of allowed values plus x-enumExtra: an object keyed by each enum value whose values hold a display string, optional description, and any extra keys mapped from x_<name> query parameters (for example x_icon=icon_url). Callers select source paths with s_value, s_label, s_description, s_format, and s_type. Schema mapping uses the s_ / x_ prefix so those keys do not overlap typical list API query parameters on the embedded endpoints. Use s_format=oneOf if you need the const / title branch layout. This keeps validators from compiling thousands of oneOf / anyOf branches while preserving display metadata.

Pipeline & internal rendering

DynamicSchemaFromSourceView follows a small pipeline: it pulls the source rows from the embedded list view (get_data_from_source_view), maps each row to output keys via get_schema_items (driven by get_fields_map, with optional get_<field>_from_item hooks), then hands the mapped rows to the fragment serializer chosen by s_format (serialize_enum_fragment / serialize_one_of_fragment, registered in schemas.format_serializers).

Internal consumers that cannot deal with two shapes (currently AlertingSchemaPropertiesAdapter, which speaks anyOf / oneOf) wrap their call in output_format_override(OUTPUT_FORMAT_ONE_OF). The override is a ContextVar checked by DynamicSchemaFromSourceView.get_output_format before s_format / default_format, so any nested $ref expansion produced during dereferencing comes back as oneOf regardless of the source view’s default. Public API endpoints are unaffected — they only see the override if their request is itself made inside one.

3. Enhanced Field Type System

V2 supports comprehensive field types with proper JSON Schema definitions:

  • Text Fields: Short text, long text, email, URL

  • Numeric Fields: Integer, number, with min/max constraints

  • Choice Fields: Single and multi-select with reference resolution

  • Date/Time Fields: Date, time, datetime with timezone support

  • Location Fields: Point and polygon geometries

  • Attachment Fields: File uploads with type restrictions

  • Collection Fields: Nested object collections

4. Modern UI Definition System

The UI system provides:

  • Field-specific configurations: Each field type has tailored UI options

  • Responsive layouts: Built-in support for different screen sizes

  • Section organization: Logical grouping of related fields

  • Accessibility support: Proper labeling and ARIA attributes

JSON Schema Structure

V2 eventtypes use a two-section structure: json for data schema and ui for interface definition.

Schema Structure

{
  "json": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "additionalProperties": false,
    "properties": {
      "field_name": {
        "type": "string",
        "title": "Field Title",
        "description": "Field description",
        "deprecated": false
      }
    },
    "required": ["field_name"],
    "type": "object"
  },
  "ui": {
    "fields": { /* UI field configurations */ },
    "sections": { /* Layout sections */ },
    "order": ["section-1"],
    "headers": {}
  }
}

Property and Field Naming Constraints

These are hard limits enforced by the meta-schema (JSON Schema Draft 2020-12) in das/activity/schemas/eventtype_meta_schemas.py. An invalid name is rejected at the API/serializer layer with a 400 error — it is never stored.

Allowed characters

Every name segment must match [a-zA-Z0-9_-]+:

Allowed

Examples

Lowercase letters a-z

species, age_of_carcass

Uppercase letters A-Z

Status, GPSFix

Digits 0-9

field1, zone3

Underscore _

cause_of_death

Hyphen -

arrest-rep, sub-type

Spaces, dots (.), slashes, and all other characters are not allowed in any name segment. There is no maximum-length constraint on property names themselves.

Top-level property keys (json.properties)

Keys in json.properties must match FIELD_NAME_PATTERN:

^[a-zA-Z0-9_-]+$

This is a single segment — dots are not permitted at the top level. The same pattern is enforced on every required array item and on condition patternProperties keys throughout the schema.

// Valid
"properties": {
  "carcassrep_species": { ... },
  "report-status": { ... }
}

// Invalid — dot not allowed at top level
"properties": {
  "parent.child": { ... }
}

Field IDs in the UI schema (ui.fields)

Keys in ui.fields match FIELD_ID_PATTERN, which extends the segment pattern to allow dot-separated paths for referencing nested fields inside a collection:

^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$

Each dot-separated segment still follows the same character rules. Use dot notation when a field inside a collection needs its own UI configuration (e.g. arrests.photo).

// Top-level field
"ui": { "fields": { "carcassrep_species": { ... } } }

// Collection sub-field
"ui": { "fields": { "arrests.photo": { ... } } }

FIELD_ID_PATTERN is also used for column item name values of type "field", the collection itemIdentifier property, and in field_parent_schema references.

Header IDs (ui.headers)

Keys in ui.headers must match HEADER_ID_PATTERN:

^header-[a-zA-Z0-9_-]+$

The header- prefix is required. Example: header-section1-title.

Section IDs (ui.sections / ui.order)

Keys in ui.sections and items in ui.order must match SECTION_ID_PATTERN:

^section-[a-zA-Z0-9_-]+$

The section- prefix is required. Example: section-basic-info.

Summary table

Context

Pattern

Example

json.properties key

^[a-zA-Z0-9_-]+$

carcassrep_species

json.required item

^[a-zA-Z0-9_-]+$

carcassrep_species

ui.fields key

^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*$

arrests.photo

ui.headers key

^header-[a-zA-Z0-9_-]+$

header-info

ui.sections key / ui.order item

^section-[a-zA-Z0-9_-]+$

section-1

Field Types

Text Fields

{
  "field_name": {
    "type": "string",
    "title": "Text Field",
    "description": "A text input field",
    "minLength": 1,
    "maxLength": 255,
    "pattern": "^[A-Za-z0-9\\s]*$"
  }
}

Numeric Fields

{
  "field_name": {
    "type": "number",
    "title": "Numeric Field",
    "description": "A numeric input field",
    "minimum": 0,
    "maximum": 100,
    "multipleOf": 0.1
  }
}

Choice Fields (Single Select)

{
  "field_name": {
    "type": "string",
    "title": "Choice Field",
    "description": "Select one option",
    "anyOf": [
      {
        "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=field_name"
      }
    ]
  }
}

Choice List Fields (Multi-Select)

{
  "field_name": {
    "type": "array",
    "title": "Choice List Field",
    "description": "Select multiple options",
    "items": {
      "anyOf": [
        {
          "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=field_name"
        }
      ]
    },
    "uniqueItems": true,
    "minItems": 1,
    "maxItems": 5
  }
}

Date/Time Fields

{
  "field_name": {
    "type": "string",
    "title": "Date Field",
    "description": "Select a date",
    "format": "date"
  }
}

Location Fields

{
  "field_name": {
    "type": "object",
    "title": "Location Field",
    "description": "Select a location",
    "properties": {
      "type": {"const": "Point"},
      "coordinates": {
        "type": "array",
        "items": {"type": "number"},
        "minItems": 2,
        "maxItems": 2
      }
    },
    "required": ["type", "coordinates"]
  }
}

Attachment Fields

An attachment field is an array of objects — each object has a required uploadId property (a UUID string) referencing an uploaded file (see Submitting Events with Attachments). deprecated, items, title, type, and uniqueItems are required; description, minItems, and maxItems (non-negative integers) are optional. The items shape is a constant enforced by the schema.

{
  "field_name": {
    "type": "array",
    "title": "Attachment Field",
    "description": "Upload files",
    "deprecated": false,
    "items": {
      "properties": {
        "uploadId": {
          "format": "uuid",
          "type": "string"
        }
      },
      "required": ["uploadId"],
      "type": "object",
      "unevaluatedProperties": false
    },
    "uniqueItems": true,
    "minItems": 0,
    "maxItems": 5
  }
}

UI Definition System

The UI system defines how fields are presented and organized in the user interface.

UI Structure

{
  "ui": {
    "fields": {
      "field_name": {
        "type": "TEXT",
        "inputType": "SHORT_TEXT",
        "placeholder": "Enter text here",
        "parent": "section-1"
      }
    },
    "sections": {
      "section-1": {
        "label": "Main Section",
        "columns": 2,
        "isActive": true,
        "leftColumn": [
          {"name": "field_name", "type": "field"}
        ],
        "rightColumn": []
      }
    },
    "order": ["section-1"],
    "headers": {}
  }
}

Field UI Types

Text Fields

{
  "type": "TEXT",
  "inputType": "SHORT_TEXT", // or "LONG_TEXT"
  "placeholder": "Enter text here",
  "parent": "section-1"
}

Numeric Fields

{
  "type": "NUMBER",
  "inputType": "NUMBER",
  "placeholder": "Enter number",
  "parent": "section-1"
}

Choice Fields

{
  "type": "CHOICE_LIST",
  "inputType": "DROPDOWN", // or "RADIO", "CHECKBOX"
  "choices": {
    "type": "EXISTING_CHOICE_LIST",
    "existingChoiceList": ["choice_field_name"],
    "eventTypeCategories": [],
    "featureCategories": [],
    "subjectGroups": [],
    "subjectSubtypes": [],
    "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP"
  },
  "placeholder": "Select option",
  "parent": "section-1"
}

Attachment Fields

{
  "type": "ATTACHMENT",
  "allowableFileTypes": ["image", "document", "video", "audio"],
  "parent": "section-1"
}

Collection Fields

{
  "type": "COLLECTION",
  "parent": "section-1"
}

Section Configuration

{
  "sections": {
    "section-1": {
      "label": "Basic Information",
      "columns": 2,
      "isActive": true,
      "leftColumn": [
        {"name": "field1", "type": "field"},
        {"name": "field2", "type": "field"}
      ],
      "rightColumn": [
        {"name": "field3", "type": "field"}
      ]
    }
  }
}

Reference Resolution System

V2 uses JSON Schema references to resolve dynamic content like choice lists.

Choice Reference URLs

Choice lists are resolved through API endpoints:

https://api.example.com/v2.0/schemas/choices.json?field=field_name

Reference Resolution Process

  1. Schema Validation: Initial validation of schema structure

  2. Reference Discovery: Identification of $ref URLs in the schema

  3. Reference Resolution: Fetching and resolving referenced content

  4. Schema Completion: Final validation of the complete schema

Resolved Choice Format

After resolution, choice references become:

{
  "anyOf": [
    {
      "const": "value1",
      "title": "Display Name 1"
    },
    {
      "const": "value2",
      "title": "Display Name 2"
    }
  ]
}

Complete Example

Wildlife Carcass Report EventType

This example demonstrates a complete V2 eventtype for wildlife carcass reporting:

{
  "json": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "additionalProperties": false,
    "properties": {
      "carcassrep_species": {
        "deprecated": false,
        "description": "Species of the animal carcass",
        "title": "Species",
        "type": "string",
        "anyOf": [
          {
            "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_species"
          }
        ]
      },
      "carcassrep_sex": {
        "deprecated": false,
        "description": "Sex of the animal",
        "title": "Sex of Animal",
        "type": "string",
        "anyOf": [
          {
            "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_sex"
          }
        ]
      },
      "carcassrep_ageofanimal": {
        "deprecated": false,
        "description": "Estimated age of the animal at death",
        "title": "Age of Animal",
        "type": "string",
        "anyOf": [
          {
            "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_ageofanimal"
          }
        ]
      },
      "carcassrep_ageofcarcass": {
        "deprecated": false,
        "description": "Estimated age of the carcass when found",
        "title": "Age of Carcass",
        "type": "string",
        "anyOf": [
          {
            "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_ageofcarcass"
          }
        ]
      },
      "carcassrep_trophystatus": {
        "deprecated": false,
        "description": "Trophy status of the animal",
        "title": "Trophy Status",
        "type": "string",
        "anyOf": [
          {
            "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_trophystatus"
          }
        ]
      },
      "carcassrep_causeofdeath": {
        "deprecated": false,
        "description": "Suspected cause of death",
        "title": "Cause of Death",
        "type": "string",
        "anyOf": [
          {
            "$ref": "https://api.example.com/v2.0/schemas/choices.json?field=carcassrep_causeofdeath"
          }
        ]
      },
      "carcassrep_notes": {
        "deprecated": false,
        "description": "Additional observations and notes",
        "title": "Notes",
        "type": "string",
        "maxLength": 1000
      }
    },
    "required": ["carcassrep_species", "carcassrep_sex"],
    "type": "object"
  },
  "ui": {
    "fields": {
      "carcassrep_species": {
        "choices": {
          "eventTypeCategories": [],
          "existingChoiceList": ["carcassrep_species"],
          "featureCategories": [],
          "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP",
          "subjectGroups": [],
          "subjectSubtypes": [],
          "type": "EXISTING_CHOICE_LIST"
        },
        "inputType": "DROPDOWN",
        "placeholder": "Select species",
        "type": "CHOICE_LIST",
        "parent": "section-1"
      },
      "carcassrep_sex": {
        "choices": {
          "eventTypeCategories": [],
          "existingChoiceList": ["carcassrep_sex"],
          "featureCategories": [],
          "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP",
          "subjectGroups": [],
          "subjectSubtypes": [],
          "type": "EXISTING_CHOICE_LIST"
        },
        "inputType": "DROPDOWN",
        "placeholder": "Select sex",
        "type": "CHOICE_LIST",
        "parent": "section-1"
      },
      "carcassrep_ageofanimal": {
        "choices": {
          "eventTypeCategories": [],
          "existingChoiceList": ["carcassrep_ageofanimal"],
          "featureCategories": [],
          "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP",
          "subjectGroups": [],
          "subjectSubtypes": [],
          "type": "EXISTING_CHOICE_LIST"
        },
        "inputType": "DROPDOWN",
        "placeholder": "Select age",
        "type": "CHOICE_LIST",
        "parent": "section-1"
      },
      "carcassrep_ageofcarcass": {
        "choices": {
          "eventTypeCategories": [],
          "existingChoiceList": ["carcassrep_ageofcarcass"],
          "featureCategories": [],
          "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP",
          "subjectGroups": [],
          "subjectSubtypes": [],
          "type": "EXISTING_CHOICE_LIST"
        },
        "inputType": "DROPDOWN",
        "placeholder": "Select carcass age",
        "type": "CHOICE_LIST",
        "parent": "section-1"
      },
      "carcassrep_trophystatus": {
        "choices": {
          "eventTypeCategories": [],
          "existingChoiceList": ["carcassrep_trophystatus"],
          "featureCategories": [],
          "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP",
          "subjectGroups": [],
          "subjectSubtypes": [],
          "type": "EXISTING_CHOICE_LIST"
        },
        "inputType": "DROPDOWN",
        "placeholder": "Select trophy status",
        "type": "CHOICE_LIST",
        "parent": "section-1"
      },
      "carcassrep_causeofdeath": {
        "choices": {
          "eventTypeCategories": [],
          "existingChoiceList": ["carcassrep_causeofdeath"],
          "featureCategories": [],
          "myDataType": "SUBJECTS_FROM_SUBJECT_GROUP",
          "subjectGroups": [],
          "subjectSubtypes": [],
          "type": "EXISTING_CHOICE_LIST"
        },
        "inputType": "DROPDOWN",
        "placeholder": "Select cause of death",
        "type": "CHOICE_LIST",
        "parent": "section-1"
      },
      "carcassrep_notes": {
        "inputType": "LONG_TEXT",
        "placeholder": "Enter additional observations...",
        "type": "TEXT",
        "parent": "section-1"
      }
    },
    "headers": {},
    "order": ["section-1"],
    "sections": {
      "section-1": {
        "columns": 2,
        "isActive": true,
        "label": "Carcass Information",
        "leftColumn": [
          {"name": "carcassrep_species", "type": "field"},
          {"name": "carcassrep_sex", "type": "field"},
          {"name": "carcassrep_ageofanimal", "type": "field"}
        ],
        "rightColumn": [
          {"name": "carcassrep_ageofcarcass", "type": "field"},
          {"name": "carcassrep_trophystatus", "type": "field"},
          {"name": "carcassrep_causeofdeath", "type": "field"}
        ]
      }
    }
  }
}

Submitting Events with Attachments

V2 event types bind file uploads to named properties in event_details (e.g. event_details.photo), in contrast to V1 where files were a flat list attached to the event via a separate endpoint. The stored value of an attachment property is an array of upload objects, each with shape {"uploadId": "<uuid>"}. On read, event_details.<field> returns the raw stored array — proxy URLs are served back only in metadata.attachments.<uuid>.files for status="complete" attachments.

Lifecycle at a glance

Uploads and event creation are decoupled — there is no chicken-and-egg here. The UUID that binds an upload to an event is client-supplied: the caller may bring its own UUID4 and use it immediately, with no round-trip needed to mint an id first. The file is uploaded to a generic, event-agnostic endpoint; the event is created (or updated) referencing that same UUID. You do not need an event id to start the upload, you do not need the file to finish uploading before you create the event, and — because the id is bring-your-own — you do not even need to have started the upload before you create the event.

1. POST /api/v1.0/usercontent/chunked-uploads/   →  obtain id (a UUID); caller may supply their own
2. PUT  /api/v1.0/usercontent/chunked-uploads/<id>/chunks/<N>/  →  upload each chunk
3. POST /api/v1.0/usercontent/chunked-uploads/<id>/complete/    →  finalize; FileContent row created
4. POST /api/v2.0/activity/events/  with event_details.<field> = [{"uploadId": "<id>"}]
5. GET  /api/v2.0/activity/events/<event-id>/  →  event_details.<field> = raw array; metadata.attachments sidecar

The same UUID flows through every step. This ordering is not mandatory: because the id is bring-your-own, the client can pre-allocate the UUID itself and POST the event (step 4) before uploading the file (steps 1–3) — the init call is only one way to obtain an id, not a prerequisite. The sole invariant is that the same UUID appears in both the upload session and the event_details slot.

Step 1 — Initiate the upload (chunked-upload protocol)

POST /api/v1.0/usercontent/chunked-uploads/ — init

Request body:

Field

Required

Description

filename

Yes

Original filename; extension must be in USERCONTENT_SETTINGS.allowed_extensions.

size

Yes

Total file size in bytes; must be 1..CHUNKED_UPLOAD_MAX_FILE_SIZE (default 500 MiB).

chunk_size

No

Preferred chunk size; server caps to the lesser of CHUNKED_UPLOAD_CHUNK_SIZE (default 2 MiB) and Django’s DATA_UPLOAD_MAX_MEMORY_SIZE minus a safety margin.

id

No

Client-supplied UUID4 for the upload. If omitted the server generates one.

POST /api/v1.0/usercontent/chunked-uploads/
Content-Type: application/json

{
  "filename": "suspect.jpg",
  "size": 482133,
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "chunk_size": 262144
}

201 Created response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "chunk_size": 262144,
  "size": 482133,
  "num_chunks": 2
}

Error codes:

Code

Reason

400

Invalid/disallowed filename extension; chunk_size over the server cap.

401

Authentication required.

409

Client-supplied id collides with a live session or a finalized row.

502

Storage init failure; body contains error_id (no internal detail exposed).

Throttled at the chunked_upload_init scope (60/min).

PUT /api/v1.0/usercontent/chunked-uploads/<id>/chunks/<N>/ — upload a chunk

Send raw bytes (Content-Type: application/octet-stream). Chunks must arrive strictly in order and cannot be parallelized — the server accepts only the next expected index (the status endpoint’s next_chunk_index) and rejects any higher index with 400 (Chunk out of order). Re-sending an already-received (lower) index is idempotent only if the bytes are byte-identical; different bytes return 400.

Code

Reason

200

Chunk accepted (204 surfaced as 200 by ExtendedJSONRenderer).

400

Index out of range; byte length mismatch; duplicate index with different bytes.

403

Session belongs to another user.

404

Session not found or expired (TTL CHUNKED_UPLOAD_SESSION_TTL_SECONDS, default 86400 s).

Throttled at the chunked_upload_chunk scope (200/min).

GET /api/v1.0/usercontent/chunked-uploads/<id>/ — poll status (resume)

Returns next_chunk_index, num_chunks, complete, size, chunk_size. Use this to resume an interrupted upload. Returns 404 if the session is missing or expired; 403 if the session belongs to another user.

POST /api/v1.0/usercontent/chunked-uploads/<id>/complete/ — finalize

After all chunks are received, finalize the upload. Creates a FileContent row (or ImageFileContent for image filenames) with id == <id>. Returns the serialized record (id, filename, icon_url, file_type, created_at, updated_at).

Code

Reason

200

Finalized; body is the serialized FileContent/ImageFileContent.

400

Upload incomplete (not all chunks received).

403

Session belongs to another user.

404

Session not found or expired.

409

Upload already finalized (duplicate complete call).

Session lifecycle: Redis-backed with TTL CHUNKED_UPLOAD_SESSION_TTL_SECONDS (default 86400 s / 24 h). Expired sessions return 404 — clients should be prepared to re-init and re-upload. Maximum file size: CHUNKED_UPLOAD_MAX_FILE_SIZE (default 500 MiB).

Step 2 — Submit the event

POST /api/v2.0/activity/events/ with application/json. Place the upload objects in event_details under the property name declared as an ATTACHMENT field in the event type’s UI schema. Each attachment value is an array of objects with a single key uploadId whose value is the UUID string.

Write-time validation is format-only: any well-formed {"uploadId": "<uuid>"} object passes regardless of whether the file exists, belongs to the same user, or matches allowableFileTypes. Only non-list values, non-dict items, malformed UUIDs, extra keys on an item, and duplicate uploadId values are rejected with 400.

POST /api/v2.0/activity/events/
Content-Type: application/json

{
  "event_type": "arrest_rep",
  "title": "Suspect detained at gate",
  "time": "2026-06-03T14:30:00Z",
  "location": {"latitude": -1.2921, "longitude": 36.8219},
  "event_details": {
    "arrestrep_name": "John Doe",
    "photo": [{"uploadId": "550e8400-e29b-41d4-a716-446655440000"}]
  }
}

photo here must be declared in the event type schema as:

"ui": {
  "fields": {
    "photo": {
      "type": "ATTACHMENT",
      "allowableFileTypes": ["image"],
      "parent": "section-1"
    }
  }
}

PATCH /api/v2.0/activity/events/<id>/ accepts the same shape for updates.

Attachments inside a collection

When the ATTACHMENT field lives inside a COLLECTION, the upload object array goes on each collection item:

"event_details": {
  "arrests": [
    {"name": "John Doe",   "arrestee_photo": [{"uploadId": "550e8400-e29b-41d4-a716-446655440000"}]},
    {"name": "Jane Smith", "arrestee_photo": [{"uploadId": "550e8400-e29b-41d4-a716-446655440001"}]}
  ]
}

Write-time validation rules

Failure

HTTP

Example response body

Value is not an array

400

{"event_details": {"photo": ["Expected an array of attachment objects."]}}

Array item is not a dict

400

{"event_details": {"photo": ["Item at index 0 must be an object with an 'uploadId' key: ..."]}}

Item missing uploadId key

400

{"event_details": {"photo": ["Item at index 0 is missing required key 'uploadId'."]}}

Item has extra keys

400

{"event_details": {"photo": ["Item at index 0 has unexpected key(s): ..."]}}

uploadId value is not a valid UUID string

400

{"event_details": {"photo": ["Item at index 0: 'uploadId' is not a valid UUID: ..."]}}

Duplicate uploadId in the array

400

{"event_details": {"photo": ["Item at index 1 is a duplicate uploadId: ..."]}}

Array shorter than minItems

400

{"event_details": {"photo": ["This field must contain at least N item(s)."]}}

Array longer than maxItems

400

{"event_details": {"photo": ["This field must contain at most N item(s)."]}}

null and missing values are accepted — an attachment property is optional unless the JSON schema marks it required. An empty array ([]) is also accepted when no minItems bound is set. Unknown UUIDs, cross-tenant UUIDs, and file-type mismatches all pass.

Metadata sidecar (write side)

On every write, the server stores a slim {} placeholder for each UUID present in an attachment slot in EventDetails.data["metadata"]["attachments"]. Removing a field from event_details drops its placeholder. The sidecar is hydrated at read time (see below) — it is never stored with resolved URLs.

Step 3 — Read back

GET /api/v2.0/activity/events/<id>/ returns event_details.<field> as the raw stored upload object array (not a proxy URL). Proxy URLs appear only in metadata.attachments.<uuid>.files for status="complete" attachments.

{
  "id": "…",
  "event_type": "arrest_rep",
  "event_details": {
    "arrestrep_name": "John Doe",
    "photo": [{"uploadId": "550e8400-e29b-41d4-a716-446655440000"}]
  },
  "metadata": {
    "attachments": {
      "550e8400-e29b-41d4-a716-446655440000": {
        "status": "complete",
        "file_type": "image",
        "files": {
          "original":   "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/",
          "icon":       "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=icon",
          "thumbnail":  "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=thumbnail",
          "large":      "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=large",
          "xlarge":     "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=xlarge"
        }
      }
    }
  }
}

The metadata key sits behind the event’s category-read permission gate — unpermitted users receive only {id, serial_number}. Socket-emit (no request context) receives {status, file_type} without the files key.

metadata.attachments — status semantics

status

Meaning

Client action

unknown

Server has never seen this UUID (no session, no DB row).

Verify the UUID is correct; re-upload if needed.

in_progress

An upload session exists but is not finalized (chunks still in flight).

Finish the upload (continue PUTting chunks and POST complete) or poll the chunked-upload status endpoint.

complete

Upload finalized; file_type is set and files is present when a request context exists.

Use files.original or the desired rendition URL.

files object schema

The files key is present only when status == "complete" and the response is serialized with a request context (HTTP responses; absent from socket emits).

File type

Keys in files

image

original, icon, thumbnail, large, xlarge

document, audio, video, other

original only

Rendition URLs have the form /api/v1.0/usercontent/<uuid>/?rendition=<name>.

file_type classification

Bucket

Determined by

image

Image filename extensions (jpg, jpeg, png, gif, tif, tiff)

document

Document extensions (pdf, docx, etc.)

audio

Audio extensions

video

Video extensions

null

Unknown extension

Download endpoint

GET /api/v1.0/usercontent/<uuid>/

Condition

Behavior

Auth required

401 for anonymous requests.

Tenant-scoped

404 for unknown or cross-tenant UUIDs.

Active MIME types (SVG, HTML, JS)

Forced to application/octet-stream with Content-Disposition: attachment.

All other types

Streamed inline with correct MIME type.

X-Content-Type-Options: nosniff

Always set.

Optional ?rendition=<name> query parameter: Serves a pre-generated image rendition. Valid names: icon, thumbnail, large, xlarge (matches the configured VERSATILEIMAGEFIELD_RENDITION_KEY_SETS["default"]). Applies to images only — a rendition request on a non-image file, or an unknown rendition name, returns 404.

Access control: Relies on UUID unguessability (122-bit UUIDv4) combined with tenant scope. There is no per-user ownership check — any authenticated user in the tenant who holds the UUID may download the file.

Worked end-to-end example

Note: the ordering below is just one valid sequence. Because the upload UUID is bring-your-own, the client may POST the event form data (step 4) before initiating or finishing the upload (steps 1–3). The only requirement is that the same UUID is used in both places.

# 1. Init upload with a client-supplied UUID
POST /api/v1.0/usercontent/chunked-uploads/
{"filename": "photo.jpg", "size": 5120, "id": "550e8400-e29b-41d4-a716-446655440000", "chunk_size": 5120}

→ 201 {"id": "550e8400-e29b-41d4-a716-446655440000", "chunk_size": 5120, "size": 5120, "num_chunks": 1}

# 2. Upload the single chunk
PUT /api/v1.0/usercontent/chunked-uploads/550e8400-e29b-41d4-a716-446655440000/chunks/0/
Content-Type: application/octet-stream
<raw bytes>

→ 200

# 3. Finalize
POST /api/v1.0/usercontent/chunked-uploads/550e8400-e29b-41d4-a716-446655440000/complete/

→ 200 {"id": "550e8400-e29b-41d4-a716-446655440000", "filename": "photo.jpg", "file_type": "image", ...}

# 4. Create the event with the upload object array in event_details
POST /api/v2.0/activity/events/
{"event_type": "arrest_rep", "title": "Arrest", "event_details": {"photo": [{"uploadId": "550e8400-e29b-41d4-a716-446655440000"}]}}

→ 201

# 5. Read back — raw upload object array in event_details, files in metadata
GET /api/v2.0/activity/events/<event-id>/

→ 200
{
  "event_details": {
    "photo": [{"uploadId": "550e8400-e29b-41d4-a716-446655440000"}]
  },
  "metadata": {
    "attachments": {
      "550e8400-e29b-41d4-a716-446655440000": {
        "status": "complete",
        "file_type": "image",
        "files": {
          "original":  "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/",
          "icon":      "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=icon",
          "thumbnail": "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=thumbnail",
          "large":     "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=large",
          "xlarge":    "https://<server>/api/v1.0/usercontent/550e8400-e29b-41d4-a716-446655440000/?rendition=xlarge"
        }
      }
    }
  }
}

Validation and Error Handling

V2 eventtypes provide comprehensive validation at multiple levels:

Schema Validation

  • JSON Schema compliance: Validates against JSON Schema 2020-12 standard

  • Reference resolution: Ensures all $ref URLs are accessible and valid

  • Field type validation: Validates field types and constraints

  • Required field validation: Ensures required fields are properly defined

UI Validation

  • Field consistency: Ensures UI field definitions match JSON schema properties

  • Section validation: Validates section structure and field references

  • Choice validation: Ensures choice field configurations are valid

Error Categories

  • Validation errors: Schema structure and format issues

  • Reference errors: Unresolvable $ref URLs

  • Rendering errors: Issues during schema resolution

  • UI errors: Interface definition problems

Best Practices

Schema Design

  • Use descriptive field names: Follow consistent naming conventions. Property names are constrained to [a-zA-Z0-9_-]+ — see Property and Field Naming Constraints for the full rules enforced by the meta-schema.

  • Provide clear titles and descriptions: Help users understand field purposes

  • Set appropriate constraints: Use min/max values, patterns, and required fields

  • Group related fields: Use consistent prefixes for related fields

Choice Management

  • Stable choice values: Use URL-safe, stable values for choices

  • Meaningful display names: Provide clear, localized display text

  • Document choice dependencies: Keep track of which eventtypes use which choices

  • Version choice lists: Plan for choice list updates and migrations

UI Design

  • Logical field ordering: Arrange fields in a logical workflow order

  • Responsive layouts: Use appropriate column configurations for different screen sizes

  • Clear placeholders: Provide helpful placeholder text for all fields

  • Accessibility: Ensure proper labeling and keyboard navigation

Performance

  • Minimize references: Use local definitions when possible

  • Cache resolved schemas: Implement caching for resolved choice references

  • Optimize choice lists: Keep choice lists focused and relevant

  • Monitor resolution time: Track reference resolution performance

Troubleshooting

Common Issues

Reference Resolution Failures

Problem: $ref URLs cannot be resolved Solution:

  • Verify API endpoints are accessible

  • Check choice field names exist in the database

  • Ensure proper authentication for API calls

  • Test reference URLs manually

Schema Validation Errors

Problem: Schema fails JSON Schema validation Solution:

  • Use a JSON Schema validator to identify issues

  • Check for missing required properties

  • Verify field type definitions are correct

  • Ensure $ref syntax is properly formatted

UI Rendering Issues

Problem: Fields not displaying correctly in the UI Solution:

  • Verify UI field definitions match JSON schema properties

  • Check section structure and field references

  • Ensure proper field types and input types

  • Validate parent section references

Choice List Problems

Problem: Choice lists not populating or showing incorrect options Solution:

  • Verify choice records exist and are active

  • Check choice field names match reference URLs

  • Ensure choice API endpoints return proper format

  • Test choice resolution independently

Debugging Tips

  1. Validate schemas: Use online JSON Schema validators

  2. Test references: Manually test $ref URLs

  3. Check logs: Review application logs for resolution errors

  4. Use development tools: Leverage browser dev tools for UI issues

  5. Test incrementally: Build and test schemas piece by piece

Migration from V1

Migration Process

  1. Analyze V1 schema: Identify template variables and choice dependencies

  2. Convert to V2 format: Replace templates with $ref references

  3. Update UI definition: Convert custom definition format to V2 UI structure

  4. Test validation: Ensure schemas validate correctly

  5. Update version: Set version = "2" in the EventType model

Migration Tools

  • Schema converter: Automated tools for converting V1 to V2

  • Validation scripts: Test converted schemas before deployment

  • Choice migration: Tools for updating choice references

  • UI converter: Convert V1 definition format to V2 UI structure

For detailed migration guidance, see the V1 to V2 Migration Guide.