Back to Abp

Model Descriptor Files

docs/en/low-code/model-json.md

10.5.020.1 KB
Original Source
json
//[doc-seo]
{
    "Description": "Define ABP Low-Code descriptor metadata and descriptor schemas for dynamic entities, pages, forms, filters, permissions, script endpoints, event handlers, background jobs, and workers."
}

Model Descriptor Files

Preview: The Low-Code System is currently in preview. The descriptor format is stable enough for evaluation and source control, but fields may change before general availability.

Low-code metadata is source-controlled as JSON descriptor files used by the Low-Code Designer and React runtime. Current generated projects keep descriptors as split files under _Dynamic; older projects may still contain an aggregate _Dynamic/model.json document. Use the Low-Code Designer for normal editing. Use this page when you need to review, generate, merge, or source-control the JSON metadata directly.

File Location

Generated low-code applications keep descriptor files in a _Dynamic folder under the application domain project. A typical project stores one JSON file per descriptor:

text
YourApp.Domain/
`-- _Dynamic/
    |-- entities/
    |   `-- Acme.Campaigns.Campaign.json
    |-- pages/
    |   `-- campaigns.json
    |-- forms/
    |   `-- campaign-form.json
    `-- permissions/
        `-- Acme.Campaigns.json

Exact folders and file names are generated by the tooling for the descriptor type. Keep the whole _Dynamic folder and the generated initializer in source control. The low-code module discovers the descriptor metadata during application startup.

JSON Schemas

ABP publishes JSON Schema definitions for the descriptor objects that make up low-code metadata. These schemas are useful when you generate descriptors, review changes in source control, or want IDE validation for split descriptor files.

The schema files live in the ABP repository under schemas/low-code. Use the branch or tag that matches your ABP version.

The schema manifest is published at:

text
https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code/manifest.json

For another version, replace rel-10.5 with the matching ABP branch or tag.

The manifest maps each descriptor collection to its descriptor schema:

Descriptor collectionDescriptor schema
enumsdefinitions/enum-descriptor.schema.json
entitiesdefinitions/entity-descriptor.schema.json
endpointsdefinitions/endpoint-descriptor.schema.json
eventHandlersdefinitions/script-event-handler-descriptor.schema.json
backgroundJobsdefinitions/script-background-job-descriptor.schema.json
backgroundWorkersdefinitions/script-background-worker-descriptor.schema.json
pageGroupsdefinitions/page-group-descriptor.schema.json
pagesdefinitions/page-descriptor.schema.json
formsdefinitions/form-descriptor.schema.json
permissionsdefinitions/permission-descriptor.schema.json

Split Descriptor Files

Use the descriptor schema directly when a descriptor is stored as its own JSON file. The descriptor object is the same shape used for one item inside the related descriptor collection.

json
{
  "$schema": "https://raw.githubusercontent.com/abpframework/abp/rel-10.5/schemas/low-code/definitions/entity-descriptor.schema.json",
  "name": "Acme.Campaigns.Campaign",
  "displayName": "Campaigns",
  "properties": []
}

For example, a file that stores one page descriptor can reference page-descriptor.schema.json, and a file that stores one form descriptor can reference form-descriptor.schema.json.

The published schemas validate individual descriptor objects, not an aggregate descriptor document. If you still maintain an aggregate _Dynamic/model.json, do not point it at entity-descriptor.schema.json, page-descriptor.schema.json, or another descriptor schema; those schemas expect one descriptor object, not the top-level arrays. For aggregate metadata, use the manifest table above as the section-level reference and validate each array item with its matching descriptor schema.

Top-Level Sections

When descriptor metadata is viewed as an aggregate document, the logical sections are page/form centered. Entities define data shape; pages and forms define the React runtime UI.

json
{
  "enums": [],
  "entities": [],
  "endpoints": [],
  "eventHandlers": [],
  "backgroundJobs": [],
  "backgroundWorkers": [],
  "pageGroups": [],
  "pages": [],
  "forms": [],
  "permissions": []
}
SectionDescription
enumsReusable enum definitions
entitiesDynamic entities, properties, relations, attachments, validations, and interceptors
endpointsJavaScript-backed custom HTTP endpoints
eventHandlersJavaScript handlers for distributed events
backgroundJobsNamed JavaScript background job handlers
backgroundWorkersScheduled JavaScript workers
pageGroupsMenu folders used by runtime pages
pagesReact runtime page definitions, including data grids, kanban, calendar, gallery, form pages, and dashboards
formsNamed form definitions referenced by pages
permissionsCustom permission definitions referenced by pages and endpoints

Enums

Define enums before properties that reference them:

json
{
  "enums": [
    {
      "name": "Acme.Campaigns.CampaignStatus",
      "values": [
        { "name": "Draft", "value": 0 },
        { "name": "Active", "value": 1 },
        { "name": "Paused", "value": 2 },
        { "name": "Completed", "value": 3 }
      ]
    }
  ]
}

Use the enum from a property with type: "enum" and enumType:

json
{
  "name": "Status",
  "type": "enum",
  "enumType": "Acme.Campaigns.CampaignStatus",
  "defaultValue": "0"
}

Entities

Entities describe the persisted data model. UI is not configured with legacy property ui objects. Use page columns and filters, and named forms, for runtime UI behavior.

json
{
  "name": "Acme.Campaigns.Campaign",
  "displayName": "Campaigns",
  "displayProperty": "Name",
  "properties": [],
  "crossFieldValidations": [],
  "interceptors": []
}
FieldDescription
nameRequired stable full entity name, for example Acme.Campaigns.Campaign
displayNameDefault plural/screen label
displayPropertyProperty shown in lookups and foreign key display values
parentParent entity name for child/detail entities
attachmentsRecord-level attachment settings
propertiesEntity property definitions
crossFieldValidationsValidation rules comparing two properties
interceptorsCreate, update, and delete lifecycle scripts

Properties

json
{
  "name": "Budget",
  "type": "money",
  "isRequired": true,
  "isUnique": false,
  "allowSetByClients": true,
  "serverOnly": false,
  "isMappedToDbField": true,
  "validators": [
    { "type": "range", "minimum": 0, "maximum": 1000000 }
  ]
}
FieldDescription
nameRequired PascalCase property name
typeProperty type; omitted means string
displayNameDefault field label; pages/forms can override it
enumTypeEnum name when type is enum
defaultValueDefault value for new records, stored as a string and converted at runtime
isRequiredRequired/not nullable backend and UI validation
isUniqueUnique value validation
serverOnlyHidden from clients, API responses, and UI metadata
allowSetByClientsWhether create/update clients may set this value
isMappedToDbFieldWhether the property is stored in the database
foreignKeyLookup relation metadata
validatorsBackend/UI validation rules

Property Types

TypeDescription
stringText
int, longWhole numbers
decimal, moneyDecimal numbers and money values
dateTime, date, timeDate/time values
booleanTrue/false
guidGUID value
enumInteger-backed enum; requires enumType
file, imageUpload metadata handled by the low-code file pipeline

File, Image, and Attachments

Use file or image properties for first-class upload fields:

json
{
  "name": "CoverImage",
  "type": "image",
  "fileAllowedContentTypes": ["image/*"],
  "fileMaxSizeBytes": 5242880,
  "imageMaxWidth": 1600,
  "imageMaxHeight": 900,
  "imageResizeMode": "fit"
}

Use entity attachments when each record can have multiple arbitrary files:

json
{
  "name": "Acme.Campaigns.Campaign",
  "attachments": {
    "isEnabled": true,
    "maxFileCount": 10,
    "maxFileSizeBytes": 5242880,
    "allowedContentTypes": ["application/pdf", "image/*"]
  }
}

Foreign Keys

json
{
  "name": "OwnerId",
  "type": "guid",
  "foreignKey": {
    "entityName": "Volo.Abp.Identity.IdentityUser",
    "displayPropertyName": "UserName",
    "access": "none"
  }
}

entityName can point to another dynamic entity or a registered reference entity. access controls Foreign Access behavior for dynamic entity relations.

Validators

json
{
  "name": "EmailAddress",
  "type": "string",
  "validators": [
    { "type": "required" },
    { "type": "email" },
    { "type": "maxLength", "length": 255 }
  ]
}

Common validators include required, minLength, maxLength, stringLength, email, emailAddress, phone, url, creditCard, regularExpression, range, min, and max. Validators can include a custom message.

Pages

Pages create runtime routes and menu entries. They also choose how entity data is rendered in React.

json
{
  "name": "campaigns",
  "title": "Campaigns",
  "icon": "fa-solid fa-bullhorn",
  "type": "dataGrid",
  "entityName": "Acme.Campaigns.Campaign",
  "group": "marketing",
  "defaultFileExportMode": 0,
  "allowFileBundleExport": true,
  "columns": [
    { "propertyName": "Name", "order": 0, "exportOrder": 0 },
    { "propertyName": "Status", "order": 1, "exportOrder": 1 },
    { "propertyName": "Budget", "order": 2, "exportOrder": 2, "exportable": false }
  ],
  "filters": [
    { "propertyName": "Name", "control": "text", "defaultOperator": "contains" },
    { "propertyName": "Status", "control": "select", "defaultOperator": "equal" }
  ],
  "createFormName": "campaign-form",
  "editFormName": "campaign-form"
}

Page columns support two independent flags:

FieldDefaultPurpose
visibletrueRenders the field in the React page view
exportOrderorderOptional page-level export order. Lower values are exported first
exportabletruePage-level export flag managed by Export Fields. Allows the field to be included in Excel, CSV, download-link columns, and file bundle export

If columns is present, export uses this list as the page-level export policy. exportable: false prevents the field from being exported even if a caller sends the field name manually. exportOrder controls default export order without changing display order. Server-only entity properties are never exportable.

Page export settings:

FieldDefaultPurpose
defaultFileExportMode0Default spreadsheet output for file/image fields. 0 = file name, 1 = metadata columns, 2 = temporary download-link columns
allowFileBundleExporttrueAllows Files (.zip) export for exportable file/image columns on the page

ZIP file bundle export only includes selected page columns that are file or image fields and are exportable. The ZIP contains manifest.csv plus files under files/{recordId}/{fieldName}/{safeFileName}.

Page typeRequired fieldsPurpose
dataGridentityNameSearchable, sortable CRUD grid
kanbanentityName, groupByPropertyCards grouped by an enum/status-like property
calendarentityName, calendarStartPropertyRecords shown on a calendar
galleryentityNameVisual/card list, optionally using galleryImageProperty
formentityName, formNameStandalone form page
dashboarddashboardDashboard visualizations

Runtime routes use the page name:

text
/dynamic/<page-name>
/dynamic/<page-name>/create
/dynamic/<page-name>/edit/<record-id>
/dynamic/<page-name>/<record-id>

Forms

Forms are named definitions referenced by pages through formName, createFormName, or editFormName.

json
{
  "name": "campaign-form",
  "entityName": "Acme.Campaigns.Campaign",
  "enableSaveAndNew": true,
  "fields": [
    { "id": "name", "label": "Name", "type": "text", "binding": "Name" },
    { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Campaigns.CampaignStatus" },
    { "id": "ownerId", "label": "Owner", "type": "lookup", "binding": "OwnerId" }
  ],
  "layout": {
    "tabs": [
      {
        "id": "main",
        "title": "Main",
        "isDefault": true,
        "groups": [
          {
            "id": "details",
            "title": "Details",
            "isDefault": true,
            "fields": [
              { "fieldId": "name", "row": 0, "colSpan": 4 },
              { "fieldId": "status", "row": 1, "colSpan": 2 },
              { "fieldId": "ownerId", "row": 1, "colSpan": 2 }
            ]
          }
        ]
      }
    ]
  }
}

Form fields can be text, textarea, number, checkbox, date, datetime, time, file, image, money, select, lookup, guid, or computed. Form rules can hide, show, disable, enable, or set values for fields/groups.

Filters

Filters are page-owned. Use control: "auto" unless you need a specific control.

Property typeTypical operators
stringcontains, equal, notEqual, startsWith, endsWith, notContains, hasValue
int, long, decimal, moneybetween, equal, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, hasValue
date, dateTime, timebetween, equal, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, hasValue
booleanAll / Yes / No value selector
enum, lookup, guidequal, notEqual, in, notIn, hasValue
file, imagehasValue with All / Yes / No

hasValue is a UI alias. At runtime, Yes maps to IsNotNull, No maps to IsNull, and All does not add a filter.

Permissions

Pages can use generated defaults or explicit permission configuration:

json
{
  "permissionConfig": {
    "view": "authenticated",
    "create": "Acme.Campaigns.Create",
    "update": "Acme.Campaigns.Update",
    "delete": "Acme.Campaigns.Delete"
  }
}

Custom permission definitions live in the top-level permissions section and can be granted through the normal ABP permission management UI.

Scripts

Interceptors

json
{
  "interceptors": [
    {
      "commandName": "Create",
      "type": "Pre",
      "javascript": "if (!args.getValue('Name')) { globalError = 'Name is required.'; }"
    }
  ]
}

See Interceptors and Scripting API.

Custom Endpoints

json
{
  "endpoints": [
    {
      "name": "GetCampaignStats",
      "route": "/api/custom/campaigns/stats",
      "method": "GET",
      "requireAuthentication": true,
      "requiredPermissions": ["Acme.Campaigns"],
      "javascript": "var count = await db.count('Acme.Campaigns.Campaign'); return ok({ total: count });"
    }
  ]
}

See Custom Endpoints.

Event Handlers, Jobs, and Workers

json
{
  "eventHandlers": [
    {
      "name": "NotifyCampaignCompleted",
      "eventName": "Acme.Campaigns.CampaignCompleted",
      "javascript": "log('Campaign completed: ' + eventData.id);"
    }
  ],
  "backgroundJobs": [
    {
      "name": "SendCampaignSummary",
      "javascript": "log('Sending summary for ' + jobData.campaignId);"
    }
  ],
  "backgroundWorkers": [
    {
      "name": "CampaignCleanup",
      "period": 3600000,
      "javascript": "log('Cleaning campaign data.');"
    }
  ]
}

Background workers require either period in milliseconds or cronExpression. See Script Actions for event handler, background job, background worker, code editor, and dry-run testing details.

Complete Example

The complete example below shows the logical aggregate shape. Split projects store each descriptor as its own file, but field shapes are the same.

json
{
  "enums": [
    {
      "name": "Acme.Campaigns.CampaignStatus",
      "values": [
        { "name": "Draft", "value": 0 },
        { "name": "Active", "value": 1 },
        { "name": "Completed", "value": 2 }
      ]
    }
  ],
  "entities": [
    {
      "name": "Acme.Campaigns.Campaign",
      "displayName": "Campaigns",
      "displayProperty": "Name",
      "properties": [
        { "name": "Name", "type": "string", "isRequired": true, "validators": [{ "type": "maxLength", "length": 128 }] },
        { "name": "Status", "type": "enum", "enumType": "Acme.Campaigns.CampaignStatus", "defaultValue": "0" },
        { "name": "Budget", "type": "money" },
        { "name": "StartDate", "type": "date" },
        { "name": "CoverImage", "type": "image", "fileAllowedContentTypes": ["image/*"] }
      ]
    }
  ],
  "forms": [
    {
      "name": "campaign-form",
      "entityName": "Acme.Campaigns.Campaign",
      "fields": [
        { "id": "name", "label": "Name", "type": "text", "binding": "Name" },
        { "id": "status", "label": "Status", "type": "select", "binding": "Status", "enumType": "Acme.Campaigns.CampaignStatus" },
        { "id": "budget", "label": "Budget", "type": "money", "binding": "Budget" },
        { "id": "startDate", "label": "Start Date", "type": "date", "binding": "StartDate" },
        { "id": "coverImage", "label": "Cover Image", "type": "image", "binding": "CoverImage" }
      ],
      "layout": {
        "tabs": [
          {
            "id": "main",
            "title": "Main",
            "isDefault": true,
            "groups": [
              {
                "id": "details",
                "title": "Details",
                "isDefault": true,
                "fields": [
                  { "fieldId": "name", "row": 0, "colSpan": 4 },
                  { "fieldId": "status", "row": 1, "colSpan": 2 },
                  { "fieldId": "budget", "row": 1, "colSpan": 2 },
                  { "fieldId": "startDate", "row": 2, "colSpan": 2 },
                  { "fieldId": "coverImage", "row": 2, "colSpan": 2 }
                ]
              }
            ]
          }
        ]
      }
    }
  ],
  "pageGroups": [
    { "name": "marketing", "title": "Marketing", "icon": "fa-solid fa-bullhorn", "order": 10 }
  ],
  "pages": [
    {
      "name": "campaigns",
      "title": "Campaigns",
      "type": "dataGrid",
      "entityName": "Acme.Campaigns.Campaign",
      "group": "marketing",
      "columns": [
        { "propertyName": "Name", "order": 0, "exportOrder": 0 },
        { "propertyName": "Status", "order": 1, "exportOrder": 1 },
        { "propertyName": "Budget", "order": 2, "exportOrder": 2, "exportable": false }
      ],
      "filters": [
        { "propertyName": "Name", "control": "text", "defaultOperator": "contains" },
        { "propertyName": "Status", "control": "select", "defaultOperator": "equal" },
        { "propertyName": "CoverImage", "control": "exists", "defaultOperator": "hasValue" }
      ],
      "createFormName": "campaign-form",
      "editFormName": "campaign-form"
    }
  ]
}

Migration Requirements

Entity shape changes require database migrations before they can be used safely:

  • New entity
  • New persisted property
  • Property type change
  • Required/nullability change
  • Unique index change

In ABP Studio, run the generated migration task for the solution. If you run the application from the command line, use the migration workflow generated by the startup template.

See Also