Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make error message more context descriptive by allowing custom messages #3211

Closed
TharmiganK opened this issue Aug 4, 2022 · 2 comments · Fixed by ballerina-platform/module-ballerina-constraint#79
Assignees
Labels
module/constraint Points/3 Team/PCM Protocol connector packages related issues Type/Improvement

Comments

@TharmiganK
Copy link
Contributor

Description:

Constraint module should support custom messages when the validation fails. This can give more descriptive message wrt to the context.

Describe your problem(s)

With the current implementation, the following is return as an error message when constraint validation fails.

import ballerina/http;
import ballerina/constraint;

type User record {
    @constraint:String {
        minLength: 1,
        maxLength: 10
    }
    string userName;
    @constraint:Int {
        minValue: 18
    }
    int age;
};

service on new http:Listener(9090) {

    resource function post users(@http:Payload User user) returns http:Created {
        return http:CREATED;
    }
}

Request 1 : curl http://localhost:9090/users -d '{"userName": "tharmi", "age": 2}' -H "content-type: application/json"
Error Message : payload validation failed: Validation failed for '$.age:minValue' constraint(s).

Request 2 : curl http://localhost:9090/users -d '{"userName": "tharmigankrish", "age": 20}' -H "content-type: application/json"
Error Message : payload validation failed: Validation failed for '$.userName:maxLength' constraint(s).

Describe your solution(s)

Having a message field in the constraint annotation to support customized error messages

Example :

import ballerina/http;
import ballerina/constraint;

type User record {
    @constraint:String {
        minLength: 1,
        maxLength: 10,
        message: "User name should not exceed 10 characters"
    }
    string userName;
    @constraint:Int {
        minValue: 18,
        message: "User should be atleast 18 years old"
    }
    int age;
};

service on new http:Listener(9090) {

    resource function post users(@http:Payload User user) returns http:Created {
        return http:CREATED;
    }
}

Request 1 : curl http://localhost:9090/users -d '{"userName": "tharmi", "age": 2}' -H "content-type: application/json"
New Error Message : payload validation failed: User should be atleast 18 years old

Request 2 : curl http://localhost:9090/users -d '{"userName": "tharmigankrish", "age": 20}' -H "content-type: application/json"
New Error Message : payload validation failed: User name should not exceed 10 characters

@TharmiganK TharmiganK added Type/Improvement Team/PCM Protocol connector packages related issues module/constraint labels Aug 4, 2022
@TharmiganK TharmiganK self-assigned this Aug 4, 2022
@TharmiganK TharmiganK moved this to Planned for Sprint in Ballerina Team Main Board Mar 31, 2023
@TharmiganK TharmiganK moved this from Planned for Sprint to BackLog in Ballerina Team Main Board Apr 3, 2023
@TharmiganK TharmiganK moved this from BackLog to Planned for Sprint in Ballerina Team Main Board Apr 17, 2023
@TharmiganK TharmiganK moved this from Planned for Sprint to In Progress in Ballerina Team Main Board May 4, 2023
@TharmiganK
Copy link
Contributor Author

TharmiganK commented May 11, 2023

Proposed Solution

The default error message can be overridden using a message field in the constraint annotation as follows:

type User record {
    @constraint:String {
        minLength: 1,
        maxLength: 10,
        message: "User name should not exceed 10 characters"
    }
    string userName;
    @constraint:Int {
        minValue: 18,
        message: "User should be atleast 18 years old"
    }
    int age;
};

The new error format

After this improvement, the constraint:Error will be structured like this:

constraint:Error {
     // message is constructed from the messages associated with the constraint annotations
     string message;
     // details will be a map where the key denotes the failed element path and the value denotes
     // the message(s) associated with that particular path constraints
     map<string|string[]> details;
     // Cause is an optional field which has the default message. This will be only populated
     // if there is at least one user-defined message
     error cause?;
}

Construction of error message

The error message is constructed by concatenating the error messages associated with the annotation. The following will be considered while constructing this message :

  • If there is a user-defined message then the trailing/leading spaces and full stop will be removed (full stop will be added last after concatenating the messages)
  • If there are more than one constraint violation then all the messages will be concatenated with , and and
  • If there are constraint violations which don't have user-defined messages, then the default error message format will be followed

Examples

Scenario 1 :

@constraint:String {
    minLength: 2,
    maxLength: 6,
    message: "Short name should be 2 to 6 characters long"
}
type ShortName string;

ShortName name = "j";
error => {
    message: "Short name should be 2 to 6 characters long",
    cause: {
        message: "Validation failed for '$:minLength' constraint(s)."
    },
    details: {
        "$" : "Short name should be 2 to 6 characters long"
    }
}

Scenario 2 :

type Person record {
    ShortName name;
    @constraint:Int {
        minValue: 18,
        message: "Person should be at least 18 years old"
    }
    int age;
};

Person person = {name: "j", age: 20};
error => {
    message: "Short name should be 2 to 6 characters long",
    cause: {
        message: "Validation failed for '$.name:minLength' constraint(s)."
    },
    details: {
        "$.name": "Short name should be 2 to 6 characters long"
    }
}

Person person = {name: "j", age: 10};
error => {
    message: "Person should be at least 18 years old and Short name should be 2 to 6 characters long",
    cause: {
        message: "Validation failed for '$.age:minValue','$.name:minLength' constraint(s)."
    },
    details: {
        "$.name": "Short name should be 2 to 6 characters long",
        "$.age": "Person should be at least 18 years old"
    }
}

Scenario 3 :

type User record {
    @constraint:String {
        minLength: 2,
        maxLength: 6,
        message: "User's name should be 2 to 6 characters long"
    }
    string name;
    @constraint:Int {
        minValue: 18
    }
    int age;
};

User user = {name: "j", age: 20};
error => {
    message: "User's name should be 2 to 6 characters long",
    cause: {
        message: "Validation failed for '$.name:minLength' constraint(s)."
    },
    details: {
        "$.name": "User's name should be 2 to 6 characters long"
    }
}

User user = {name: "j", age: 10};
error => {
    message: "User's name should be 2 to 6 characters long and Validation failed for '$.age:minValue' constraint(s).",
    cause: {
        message: "Validation failed for '$.age:minValue','$.name:minLength' constraint(s)."
    },
    details: {
        "$.name": "User's name should be 2 to 6 characters long",
        "$.age": "Validation failed for '$.age:minValue' constraint(s)."
    }
}

Scenario 4 :

type Employee record {
    @constraint:String {
        minLength: 2,
        maxLength: 6,
        message: "Employee's name should be 2 to 6 characters long"
    }
    string name;
    @constraint:Int {
        minValue: 18
    }
    int age;
    @constraint:Array {
        maxLength: 2,
        message: "Employee should have at most 2 employees"
    }
    Employee[] employees;
};

Employee employee = {
    name: "john", 
    age: 20, 
    employees: [
        {name: "joe", age: 20, employees: []},
        {name: "rey", age: 25, employees: []},
        {name: "jay", age: 30, employees: []}
    ]
};

error => {
    message: "Employee should have at most 2 employees",
    cause: {
        message: "Validation failed for '$.employees:maxLength' constraint(s)."
    },
    details: {
        "$.employees": "Employee should have at most 2 employees"
    }
}

Employee employee = {
    name: "john", 
    age: 20, 
    employees: [
        {name: "joe", age: 15, employees: []},
        {name: "rey", age: 25, employees: []}
    ]
};

error => {
    message: "Validation failed for '$.employees[0].age:minValue' constraint(s).",
    details: {
        "$.employees[0].age": "Validation failed for 'minValue' constraint(s)."
    }
}

Employee employee = {
    name: "john", 
    age: 20, 
    employees: [
        {name: "j", age: 20, employees: []},
        {name: "rey", age: 25, employees: []}
    ]
};

error => {
    message: "Validation failed for '$.employees[0].name:minLength' constraint(s)."
    details: {
        "$.employees[0].name": "Employee's name should be 2 to 6 characters long"
    }
}

Further improvements

The error message can be improved further by allowing expression in them which will be populated at runtime. For example:

type User record {
    @constraint:String {
        minLength: 1,
        maxLength: 10,
        message: "User name: ${value} should not exceed ${maxLength} characters"
    }
    string userName;
    @constraint:Int {
        minValue: 18,
        message: "User with age ${value} is not allowed, user should be atleast ${minValue} years old"
    }
    int age;
};

@TharmiganK
Copy link
Contributor Author

TharmiganK commented May 16, 2023

@shafreenAnfar and I had a discussion on the above proposal and decided on the following changes. In this issue, we are trying to answer this question: How to override the error message when there is a particular constraint failure?. So the following changes are more appropriate than the previously proposed solution and they represent the bare minimum requirements.

  1. Since constraints are represented as the fields of the annotation record, to support custom messages we can change the field type to be union with a record type with a message field like this:

    // The new constraint annotation record definition
    type StringConstraints record {
        int|record{|int value; string message;|} minLength?;
        int|record{|int value; string message;|} maxLength?;
        int|record{|int value; string message;|} length?;
        string:regexp|record{|int value; string message;|} pattern?;
    };
    
    // Sample Use case
    type User record {
        @constraint:String {
            minLength: { value: 1, message: "User name should have at least one character" },
            maxLength: { value: 10, message: "User name should not exceed ten characters" }
        }
        string userName;
        @constraint:Int {
            minValue: { value: 18, message: "User should be at least 18 years old" }
        }
        int age;
    };
  2. Since it is the user's decision to override the error message there is no need to have a cause with the default error message.

  3. Currently there is no requirement to populate the details field in error. Therefore, we don't need to comeuppance with a new error structure. The constraint:Error will be just an error with an error message.

  4. When there is a failure in one of the members in the record, the error message will have the JSON path with the custom error like this:

    type Employee record {
        @constraint:String {
            minLength: { value: 2, message: "Employee's name should have at least two characters" },
            maxLength: { value: 6, message: "Employee's name should not exceed six characters" }
        }
        string name;
        @constraint:Int {
            minValue: 18
        }
        int age;
        @constraint:Array {
            maxLength: { value: 2, message: "Employee should have at most 2 employees" }
        }
        Employee[] employees;
    };
    
    Employee employee = {
        name: "john", 
        age: 20, 
        employees: [
            { name: "j", age: 20, employees: [] },
            { name: "rey", age: 25, employees: [] }
        ]
    };
    
    error => {
        message: "'$.employees[0]': Employee's name should have at least two characters"
    }

@github-project-automation github-project-automation bot moved this from PR Sent to Done in Ballerina Team Main Board Jun 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment