External validators
Magnolia’s New UI Forms engine features a robust validation system that can connect to both internal Java-based services and external, custom-built web services. This allows you to keep simple, content-related validations inside Magnolia while offloading complex business logic to dedicated microservices.
How it works
The validation system comprises several components that work together to route and handle validation requests.
- 
A Content App Frontend (built with React/TypeScript) renders the form, handles client-side validation, and sends validation requests to the appropriate endpoint. 
- 
A Form Schema Service (Java) provides the form definitions, including validator configurations, from the YAML configuration. 
- 
A Form Content Handler (Java REST API) manages form data CRUD operations, interacting with the Validation Dispatcher for content validation. 
- 
Validators - 
All validators, whether internal or external, adhere to the same API contract, receiving a standard request payload and returning a standard response. - 
For internal validators, the request is routed to the Validation Dispatcher (a Java REST API implemented by the info.magnolia.warp.engine.form.endpoint.ValidationEndpointclass). The dispatcher uses a Validator Provider Registry (Java) to route requests to specific internal validator services, such as the Node Name Validator Service, which validates against the JCR repository.
- 
For external validators, the request should be routed through a CDN to the customer’s hosted service, based on a unique provider ID in the URL (for example, {provider}.example.com/…). The customer’s validator service (for example, hosted on Kubernetes) routes requests to specific integrations, such as Shopify or an ERP system, based on the validator ID.
 
- 
 
- 
| The core of Magnolia’s internal routing is the  form-content-handler/restEndpoints/validation-endpoint.yaml  | 
Validator definition
Everything starts in your form definition YAML.
| To use a external validator, you add an entry to the validatorslist with the nameremote. | 
Properties
| Property | Description | ||
|---|---|---|---|
| 
 | required The type of validator.
Set to  | ||
| 
 | required The unique identifier for the specific validator type (for example,  | ||
| 
 | optional The identifier for the external service (for example,  | ||
| 
 | optional The default error message to display if validation fails (for example,  | ||
| 
 | optional A map of additional configuration specific to the validator (for example,  
 | 
form:
  fields:
    - name: productSku
      label: 'Product SKU'
      type: 'textfield'
      validators:
        - name: 'remote'
          validatorId: 'sku-exists-validator' (1)
          provider: 'my-erp-system' (2)
          errorMessage: 'This SKU does not exist in the ERP.'
          config: (3)
            region: 'eu-west-1'| 1 | This ID is used in the URL path: /validate/{validatorId}. | 
| 2 | The providerproperty is crucial. It determines the request’s destination URL.
If omitted, Magnolia assumes an internal validator.
When present, the frontend uses it to construct the external URL. | 
| 3 | You can pass a static config to your remote service. | 
Validation payload and routing
Based on the form definition, the frontend sends a request containing a standardized payload to the appropriate destination.
The validation request payload
The body of the POST request is a JSON object with a consistent structure, defined by the ValidationRequest.java and ValidationContext.java records.
The validator ID is specified in the URL path (/validate/{validatorId}), and the provider ID is used to determine the routing destination for external validators (for example, {provider}.example.com).
Properties
| Property | Description | ||
|---|---|---|---|
| 
 | required The name or path of the field being validated (for example,  | ||
| 
 | required for external validators The current value of the field being validated, extracted from the form content (for example,  
 | ||
| 
 | required A JSON object representing the entire form’s current data, useful for cross-field validation. Example: 
 | ||
| 
 | required The static  | ||
| 
 | required Standardized information about the request context. It contains the following fields: 
 | 
{
  "fieldPath": "productSku",
  "fieldValue": "PROD-123",
  "content": {
    "productSku": "PROD-123",
    "productName": "My Awesome Product",
    "stock": 100
  },
  "context": {
    "itemId": "d14950fc-7fc2-4631-92f1-b6f60a1fe40a",
    "contentType": "products-app",
    "mode": "EDIT",
    "locale": "en"
  },
  "config": {
    "region": "eu-west-1"
  }
}Routing the request
- 
For Internal Validators (no providerspecified): The request is sent to Magnolia’s local endpoint at/warp/v0/validate/{validatorId}, where the Validation Dispatcher routes it to the appropriate internal validator service via the Validator Provider Registry.
- 
For External Validators (a providerspecified): The request is sent to an external URL likehttps://{provider}.example.com/validate/{validatorId}, routed through the CDN to the customer’s validator service.
Validation response
The validation response, returned by both internal and external validators, is a JSON object that adheres to the API contract defined in the remote-validator-api.yaml specification.
It contains the following fields:
- 
isValid(boolean, required): Indicates whether the field value passes validation (truefor valid,falsefor invalid).
- 
message(string, optional): A custom error message to display when validation fails. If not provided, the frontend uses the defaulterrorMessagefrom the form YAML (for example, "This SKU does not exist in the ERP.").
- 
validatorId(string, required): Echoes the validator ID from the request’s URL path (for example,sku-exists-validator) for verification and debugging. In some internal contexts, this may be referred to asvalidatorType.
The response can represent three scenarios:
- 
Successful validation: { "isValid": true, "validatorId": "sku-exists-validator" }
- 
Failed validation with custom message: { "isValid": false, "validatorId": "sku-exists-validator", "message": "SKU 'PROD-123' not found in region eu-west-1." }
- 
Failed validation without custom message, in which case the frontend uses the default message from the form YAML: { "isValid": false, "validatorId": "sku-exists-validator" }
Implementing a custom validator service
Your external service must be able to process the request payload detailed above and return a simple JSON response.
Your service must:
- 
Be hosted at the public URL your providerID routes to (for example,{provider}.example.com).
- 
Expose an endpoint for POSTrequests at the path/validate/{validatorId}.
- 
Process the ValidationRequestJSON payload.
- 
Return a ValidationResponseJSON object containingisValid(boolean),validatorId(string), and an optionalmessage(string).
const express = require('express');
const app = express();
app.use(express.json());
app.post('/validate/sku-exists-validator', async (req, res) => { (1)
  const { fieldPath, fieldValue, content, context, config } = req.body; (2)
  const validatorId = req.params.validatorId; (3)
  console.log(`Validating in ${context.mode} mode for item ${context.itemId}.`); (4)
  const skuExists = await checkErpSystem(config.region, fieldValue); (5)
  if (skuExists) { (6)
    res.json({
      isValid: true,
      validatorId: validatorId
    });
  } else {
    res.status(200).json({
      isValid: false,
      validatorId: validatorId,
      message: `SKU '${fieldValue}' not found in region ${config.region}.`
    });
  }
});
async function checkErpSystem(region, sku) { (7)
  console.log(`Checking SKU ${sku} in ERP region ${region}...`); (8)
  return sku === 'PROD-123'; (9)
}
app.listen(3000, () => console.log('Custom validator service running on port 3000')); (10)| 1 | The service listens for POST requests at the /validate/sku-exists-validatorendpoint. | 
| 2 | Destructure the request payload to extract fieldPath,fieldValue,content,context, andconfig. | 
| 3 | Extract the validatorIdfrom the URL path parameter. | 
| 4 | Log the validation context for debugging, using the modeanditemIdfrom the request’scontext. | 
| 5 | Call the checkErpSystemfunction to validate the SKU against the ERP system, using theregionfromconfigand thefieldValue. | 
| 6 | Send the structured response adhering to the API contract, returning isValid: trueif the SKU exists, orisValid: falsewith a custom error message if it doesn’t. | 
| 7 | Define an async function to simulate checking the SKU against an external ERP system. | 
| 8 | Log the SKU and region for debugging purposes. | 
| 9 | Return trueonly if the SKU matches 'PROD-123' (simulated logic). | 
| 10 | Start the service on port 3000, running on the customer’s infrastructure. |