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
$refsystem replaces custom template variablesType 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 |
|
Uppercase letters |
|
Digits |
|
Underscore |
|
Hyphen |
|
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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¶
Schema Validation: Initial validation of schema structure
Reference Discovery: Identification of
$refURLs in the schemaReference Resolution: Fetching and resolving referenced content
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 |
|---|---|---|
|
Yes |
Original filename; extension must be in |
|
Yes |
Total file size in bytes; must be |
|
No |
Preferred chunk size; server caps to the lesser of |
|
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; |
401 |
Authentication required. |
409 |
Client-supplied |
502 |
Storage init failure; body contains |
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 |
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 |
|
Array item is not a dict |
400 |
|
Item missing |
400 |
|
Item has extra keys |
400 |
|
|
400 |
|
Duplicate |
400 |
|
Array shorter than |
400 |
|
Array longer than |
400 |
|
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¶
|
Meaning |
Client action |
|---|---|---|
|
Server has never seen this UUID (no session, no DB row). |
Verify the UUID is correct; re-upload if needed. |
|
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. |
|
Upload finalized; |
Use |
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 |
|---|---|
|
|
|
|
Rendition URLs have the form /api/v1.0/usercontent/<uuid>/?rendition=<name>.
file_type classification¶
Bucket |
Determined by |
|---|---|
|
Image filename extensions (jpg, jpeg, png, gif, tif, tiff) |
|
Document extensions (pdf, docx, etc.) |
|
Audio extensions |
|
Video extensions |
|
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 |
All other types |
Streamed inline with correct MIME type. |
|
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
$refURLs are accessible and validField 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
$refURLsRendering 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
$refsyntax 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¶
Validate schemas: Use online JSON Schema validators
Test references: Manually test
$refURLsCheck logs: Review application logs for resolution errors
Use development tools: Leverage browser dev tools for UI issues
Test incrementally: Build and test schemas piece by piece
Migration from V1¶
Migration Process¶
Analyze V1 schema: Identify template variables and choice dependencies
Convert to V2 format: Replace templates with
$refreferencesUpdate UI definition: Convert custom definition format to V2 UI structure
Test validation: Ensure schemas validate correctly
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.