-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- 1. Introduction
- 2. Getting Started
- 3. Declaring and applying corrections
- 4. Grouping corrections
- 5. Creating custom corrections
- 6. Integrating with Jakarta Bean Validation
The library provides a framework for automatically correcting inconsistent or invalid data using annotation-based correction rules. It does not perform validation itself, but can complement Bean Validation to correct data when constraints fail.
The primary goals of this specification are:
- Language compatibility: Fully compatible with both Java and Kotlin projects
- Annotation-based: Provide a simple, declarative way to define correction rules using annotations
- Jakarta Bean Validation integration: Work seamlessly with Jakarta Bean Validation constraints
- Extensibility: Allow developers to create custom correction logic
- Type safety: Ensure full type safety with generic correction appliers
- Group support: Enable conditional corrections based on validation groups
- Constraint filtering: Allow corrections to target specific validation constraints
Relationship to Jakarta Bean Validation:
This library is inspired by and designed to complement Jakarta Bean Validation. It retains similar APIs and concepts such as:
- Groups: Apply corrections conditionally based on validation groups
- Group sequences: Define ordered execution of correction groups
- Payload: Attach metadata to correction annotations
- Constraint integration: React to specific constraint violations
Add the library dependency to your project: Maven:
<dependency>
<groupId>io.github.akash-kansara</groupId>
<artifactId>modak-core</artifactId>
<version>VERSION</version>
</dependency>Gradle (Kotlin DSL):
dependencies {
implementation("io.github.akash-kansara:modak-core:$VERSION")
}Example: Class User with corrections
import io.github.akashkansara.modak.api.correction.DefaultValue;
import io.github.akashkansara.modak.api.correction.RegexReplace;
import io.github.akashkansara.modak.api.correction.Trim;
public class User {
@Trim
@DefaultValue(strValue = "Anonymous")
public String name;
@DefaultValue(intValue = 18)
public Integer age;
public String role;
@RegexReplace(
regexPattern = "[^a-zA-Z0-9@._-]",
replaceStr = ""
)
public String email;
public User(String name, Integer age, String role, String email) {
this.name = name;
this.age = age;
this.role = role;
this.email = email;
}
}NOTE: If you're using getter/setter methods, you can annotate the getter instead of fields
The @Trim, @DefaultValue and @RegexReplace annotations are used to declare the corrections which should be applied to the fields of a User instance:
- If
nameis null, assign a default value of "Anonymous" - Trim leading and trailing whitespace from
name - If
ageis null, assign a default value of 18 - Replace any characters from
emailthat match the specified regex pattern
import io.github.akashkansara.modak.api.CorrectionResult;
import io.github.akashkansara.modak.api.Corrector;
import io.github.akashkansara.modak.core.CorrectorFactory;
public class Main {
public static void main(String[] args) {
Corrector corrector = CorrectorFactory.buildCorrector();
User user = new User(" John Doe ", null, null, "example@com!pany.com");
var result = corrector.correct(user);
if (result.isSuccess()) {
CorrectionResult.Success<User> successResult = (CorrectionResult.Success<User>) result;
System.out.println(successResult.getAppliedCorrections().size());
System.out.println(user); // User{name='John Doe', age=18, role=null, email='example@company.com'}
} else {
CorrectionResult.Failure failure = ((CorrectionResult.Failure) result);
System.out.println(failure.getError().getMessage());
System.out.println(failure.getError().getCause());
System.out.println(failure.getError().getAppliedCorrections());
}
}
}The correct() method returns CorrectionResult which has a property isSuccess that indicates whether correction was successful.
When isSuccess is true, you can safely cast the result to CorrectionResult.Success<User>. This object has getAppliedCorrections API returns a list of AppliedCorrection which you can use to iterate over applied corrections.
When isSuccess is false, you can safely cast the result to CorrectionResult.Failure. This object has getError API which you can use to get details about the failure. This object also has getAppliedCorrections API which you can use to iterate over the list of corrections that were applied before the failure occurred.
Corrections are declared using annotations. There are 3 types of corrections:
- field / property corrections (Can be applied to fields or getter methods)
- container element corrections (List, Map, Array element corrections)
- class corrections
@MyCustomUserCorrection(
defaultRole = "DEFAULT",
adminRole = "ADMIN"
)
public class User {
@Trim
@DefaultValue(strValue = "Anonymous")
public String name;
public String role;
@DefaultValue(intValue = 18)
public Integer age;
@Trim(
correctionTarget = CorrectionTarget.CONTAINER_ELEMENT
)
public List<String> contactNumbers;
}
class Main {
public static void main(String[] args) {
Corrector corrector = CorrectorFactory.buildCorrector();
User user = new User(null, null, null, Arrays.asList(" +1-555-123-4567 ", "555-987-6543\n"));
var result = corrector.correct(user);
CorrectionResult.Success<User> successResult = (CorrectionResult.Success<User>) result;
System.out.println(user);
// User{
// name='Anonymous',
// age=18,
// role='DEFAULT',
// contactNumbers=['+1-555-123-4567', '555-987-6543']
// }
}
}-
@MyCustomUserCorrectionis a class-level correction that applies to the entire User object -
@Trimand@DefaultValueare field-level corrections that apply to the respective fields -
@Trim(correctionTarget = CorrectionTarget.CONTAINER_ELEMENT)is a container element correction that applies to each element in thecontactNumberslist
Corrections are inherited:
public class BaseUser {
@Trim
@DefaultValue(strValue = "Anonymous")
public String name;
}
public class User extends BaseUser {
@DefaultValue(intValue = 18)
public Integer age;
}
// User.name will have @Trim and @DefaultValue corrections inherited from BaseUserObject graph traversal and nested corrections:
public class Headquarters {
@Truncate(length = 100)
public String address;
@DefaultValue(intValue = 2000)
public Integer establishedYear;
}
public class Company {
@Trim
public String name;
@CorrectNested
public Headquarters headquarters;
}
// Correction:
Headquarters hq = new Headquarters(" 123 Main St ", null);
Company company = new Company(" Acme Corp ", hq);
CorrectionResult<Company> result = corrector.correct(company);
CorrectionResult.Success<Company> successResult = (CorrectionResult.Success<Company>) result;
System.out.println(company);
// Company{
// name='Acme Corp',
// headquarters=Office{
// address='123 Main St',
// establishedYear=2000
// }
// }Here, @CorrectNested on the headquarters field of Company enables automatic traversal into the Headquarters object and applies its corrections.
Object graph traversal and nested corrections for container elements:
public class Employee {
public Boolean isManager;
@Trim
@DefaultValue(strValue = "Unknown Employee")
public String name;
@DefaultValue(intValue = 18)
public Integer age;
}
public class Branch {
@Trim
public String name;
@ManagerCorrection( // Targets each Employee in the list
correctionTarget = CorrectionTarget.CONTAINER_ELEMENT
)
@RemoveDuplicateEmployeesCorrection // Targets the entire list of employees
@CorrectNested // Enables nested correction for each Employee in the list
public List<Employee> employees;
}
// Correction:
Employee emp1 = new Employee(null, null, null);
Employee emp2 = new Employee(true, " Alice ", 30);
Employee emp3 = new Employee(true, " Alice ", 30); // Duplicate
Branch branch = new Branch(" Branch 1 ", Arrays.asList(emp1, emp2, emp3));
CorrectionResult<Branch> result = corrector.correct(branch);
System.out.println(branch);
// Branch{
// name='Branch 1',
// employees=[
// Employee{isManager=false, name='Unknown Employee', age=18},
// Employee{isManager=true, name='Alice', age=30}
// ]
// }Assuming that:
-
@ManagerCorrectionis a custom correction that setsisManagertofalseif it's null -
@RemoveDuplicateEmployeesCorrectionis a custom correction that removes duplicate employees based onnameandage
Here, @CorrectNested on the employees field of Branch enables automatic traversal into each Employee object in the list and applies their corrections. The @ManagerCorrection applies to each Employee in the list and @RemoveDuplicateEmployeesCorrection applies to the entire list.
The AppliedCorrection class provides details about each correction that was applied during the correction process:
- propertyPath: The path to the property that was corrected (e.g., "name", "headquarters.address", "employees[0].name")
-
correctionAnnotation: The annotation instance that triggered the correction (e.g.,
@Trim,@DefaultValue) - oldValue: The original value before correction
- newValue: The new value after correction
-
correctionApplierClass: The class of the
CorrectionApplierthat performed the correction (e.g.,TrimCorrectionApplier,RemoveDuplicateEmployeesCorrectionApplier)
The library provides several built-in correction annotations such as:
DefaultValue
The @DefaultValue annotation sets a default value for null fields. It supports multiple data types including String, Integer, Long, Double, Float, Boolean, Character, Byte, Short, and Enum types.
Trim
The @Trim annotation removes leading and trailing whitespace from string fields.
Truncate
The @Truncate annotation limits the length of string fields by truncating excess characters from either the start or end, defaulting to truncating from the end.
RegexReplace
The @RegexReplace annotation replaces text in string fields that match a specified regex pattern with a given replacement string.
The correct method also takes a var-arg argument of groups. Groups allow you to restrict set of corrections that should be applied.
Example:
public interface BasicCorrection {
}
public interface RoleCorrection {
}
@MyCustomUserCorrection(
defaultRole = "DEFAULT",
adminRole = "ADMIN",
groups = {RoleCorrection.class}
)
public class User {
@Trim(
groups = {BasicCorrection.class}
)
@DefaultValue(
strValue = "Anonymous",
groups = {BasicCorrection.class}
)
public String name;
public String role;
@DefaultValue(
intValue = 18,
groups = {BasicCorrection.class}
)
public Integer age;
}
User user = new User(null, null, null, null);
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(user);
System.out.println(result.getAppliedCorrections().size()); // 0
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(
user,
BasicCorrection.class
);
System.out.println(result.getAppliedCorrections().size()); // 3
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(
user,
BasicCorrection.class, RoleCorrection.class // Corrects BasicCorrection first and then RoleCorrection
);
System.out.println(result.getAppliedCorrections().size()); // 4If all corrections are explicitly assigned to groups, calling correct() without specifying groups applies none of them. To get corrections by default, either omit the groups parameter in annotations, or use DefaultGroup.
Groups can also inherit from other groups. This allows you to create a hierarchy of groups and apply corrections based on that hierarchy.
public interface BasicCorrection {
}
public interface RoleCorrection extends BasicCorrection {
}
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(
user,
RoleCorrection.class // Corrects BasicCorrection first and then RoleCorrection
);
System.out.println(result.getAppliedCorrections().size()); // 4You might have a requirement to apply corrections in a specific order. For example, you might want to apply basic corrections first and then role-related corrections.
To enforce this, you can use GroupSequence annotation.
import io.github.akashkansara.modak.api.GroupSequence;
public interface BasicCorrection {
}
public interface RoleCorrection {
}
@GroupSequence({BasicCorrection.class, RoleCorrection.class})
public interface OrderedCorrections {
}
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(
user,
OrderedCorrections.class // Corrects BasicCorrection first and then RoleCorrection
);
System.out.println(result.getAppliedCorrections().size()); // 4To create a custom correction, 2 things are needed:
- A correction annotation
- Implement a correction applier
Let's consider the following model:
public class Phone {
public String countryCode;
public String number;
public Phone(String countryCode, String number) {
this.countryCode = countryCode;
this.number = number;
}
}Now, if we wanted to create a custom correction that adds a default value to country code if it's null, we would do the following:
import io.github.akashkansara.modak.api.Correction;
import io.github.akashkansara.modak.api.CorrectionTarget;
@Target({ElementType.TYPE, ElementType.FIELD})
@Correction(correctedBy = {PhoneCorrectionApplier.class})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneCorrection {
String defaultCountryCode() default "+1";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
CorrectionTarget correctionTarget() default CorrectionTarget.PROPERTY;
}Implementation of the correction applier:
import io.github.akashkansara.modak.api.CorrectionApplier;
import io.github.akashkansara.modak.api.CorrectionApplierContext;
import io.github.akashkansara.modak.api.CorrectionApplierResult;
public class PhoneCorrectionApplier implements CorrectionApplier<PhoneCorrection, Phone> {
private String defaultCountryCode;
@Override
public void initialize(PhoneCorrection annotation) {
this.defaultCountryCode = annotation.defaultCountryCode();
}
@Override
public CorrectionApplierResult<Phone> correct(Phone phone, CorrectionApplierContext context) {
if (phone.countryCode == null) {
Phone newPhone = new Phone(this.defaultCountryCode, phone.number);
return new CorrectionApplierResult.Edited<>(phone, newPhone);
} else {
return new CorrectionApplierResult.NoChange<>();
}
}
}Simple usage:
public class User {
public String name;
@PhoneCorrection()
public Phone phone;
public User(String name, Phone phone) {
this.name = name;
this.phone = phone;
}
}
User user = new User("John Doe", new Phone(null, "555-123-4567"));
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(user);
System.out.println(user); // User{name='John Doe', phone=Phone{countryCode='+1', number='555-123-4567'}}
System.out.println( // Phone{countryCode=null, number='555-123-4567'}
successResult.getAppliedCorrections().get(0).getOldValue()
);
System.out.println( // Phone{countryCode='+1', number='555-123-4567'}
successResult.getAppliedCorrections().get(0).getNewValue()
);Container element correction:
public class User {
public String name;
@PhoneCorrection(
defaultCountryCode = "+45",
correctionTarget = CorrectionTarget.CONTAINER_ELEMENT
)
public List<Phone> phones;
public User(String name, List<Phone> phones) {
this.name = name;
this.phones = phones;
}
}
User user = new User("John Doe", Arrays.asList(
new Phone(null, "555-123-4567"),
new Phone("+44", "020 7946 0958")
));
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(user);
System.out.println(user); // User{name='John Doe', phones=[Phone{countryCode='+45', number='555-123-4567'}, Phone{countryCode='+44', number='020 7946 0958'}]}Corrections can be applied to classes as well:
@UserCorrection(
defaultRole = "DEFAULT",
adminRole = "ADMIN"
)
public class User {
...
}The library can optionally integrate with Jakarta Bean Validation. When integrated, you can pass constraint violations to correct(), and Modak will apply only corrections relevant to those violations.
You can additionally supply groups as well to further control sequence of corrections as discussed above.
public class User {
@NotNull
@DefaultValue(
strValue = "Anonymous",
constraintFilter = {NotNull.class} // Only apply if NotNull constraint fails
)
public String name;
@Min(18)
@MinAgeCorrection(
value = 18,
constraintFilter = {Min.class} // Only apply if Min constraint fails
)
public Integer age;
}
User user = new User(null, 15);
Set<ConstraintViolation<User>> violations = validator.validate(user);
CorrectionResult.Success<User> result = (CorrectionResult.Success<User>) corrector.correct(
user,
violations // Only apply corrections relevant to these violations
);
System.out.println(user); // User{name='Anonymous', age=18}