Validating JSON Payloads in Ballerina HTTP Services using Record Types
A technical guide on implementing JSON payload validation in Ballerina using record types and error-handling resources to return structured HTTP 400 responses.
28 Sept 2026, 01:19 UTC

Handling Payload Validation Errors
When building REST APIs, ensuring that incoming JSON matches a specific schema is critical to prevent runtime crashes and data corruption. In Ballerina, the most efficient way to handle this is by binding the request payload to a strongly-typed record. When a payload fails to match the record definition, Ballerina throws a validation error that can be intercepted to return a structured HTTP 400 response rather than a generic server error.
Prerequisites
- Ballerina Swan Lake (version 2201.0 or later) installed and configured in your system PATH.
- A text editor or IDE with the Ballerina extension.
- Basic understanding of Ballerina's
serviceandresourcesyntax.
Implementation Procedure
1. Define the Data Schema
Create a record type to represent the expected JSON structure. By defining fields with specific types (e.g., string, int), Ballerina automatically performs type checking during the decoding process.
import ballerina/http;
import ballerina/log;
// Define the expected structure of the User payload
public type User record {
string name;
int age;
};
2. Create the Service and Resource
Implement a service that listens on a specific port. Use a resource function that accepts the User record as a parameter. To handle validation failures, implement a separate resource or an error handler that catches the mapping error.
service / on new http:Listener(9090) {
// Primary resource for valid payloads
resource function post users(User user) returns http:Response {
log:printInfo("Received valid user: " + user.toString());
return http:Response(200);
}
// Resource to catch validation errors for the same path
// This acts as a fallback when the 'User' type cannot be satisfied
resource function post users(error err) returns http:Response {
log:printError("Validation failed: " + err.toString());
// Return a structured JSON error response
return http:Response(400).setPayload(json {
"detail": "Invalid request payload: " + err.message
});
}
}
3. Deploy the Service
Execute the service from your terminal using the bal run command. This command compiles the code and starts the HTTP listener.
# Run from the directory containing your .bal file
bal run users_service.bal
Verify that the output confirms the service is active: Service started on http://localhost:9090.
Verification and Testing
Testing a Valid Request
Run the following curl command to send a payload that matches the User record:
curl -X POST http://localhost:9090/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","age":30}'
Expected Result: HTTP 200 OK and a log entry in the terminal showing the user data.
Testing an Invalid Request
Send a payload with a missing required field or an incorrect data type (e.g., sending a string for age):
curl -X POST http://localhost:9090/users \
-H "Content-Type: application/json" \
-d '{"name":"Bob","age":"thirty"}'
Expected Result: HTTP 400 Bad Request with a body similar to {"detail": "Invalid request payload: ..."}.
Recovery and Troubleshooting
Port Conflicts
If the service fails to start due to a BindException (port already in use), change the port in the http:Listener constructor:
service / on new http:Listener(8080) { ... }
Debugging Validation Logic
If the service returns a 500 error instead of a 400, it may be an unhandled exception. Run the service in debug mode to inspect the stack trace:
bal run --debug users_service.bal
Limitations
- Complex Validation: Record types only validate structure and basic types. Business logic validation (e.g., ensuring
ageis greater than 0) must be implemented manually within the resource function. - Blocking I/O: This implementation is synchronous. For high-throughput services, avoid blocking calls inside the resource to prevent exhausting the listener's thread pool.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.