Design by contract support for Groovy with @Invariant, @Requires, and @Ensures annotations
—
Contract annotations define design-by-contract specifications using closure expressions. All annotations are in the groovy.contracts package and work with AST transformations to inject runtime assertion checks.
Important: All contract annotations (except @Contracted) are marked with @Incubating, indicating they are subject to breaking changes in future versions until the API is stabilized.
@Target([ElementType.PACKAGE, ElementType.TYPE])
@Retention(RetentionPolicy.RUNTIME)
@interface ContractedEnables contract processing for packages or classes. Must be applied before using other contract annotations.
Usage Example:
@Contracted
package com.example.contracts
import groovy.contracts.*@Target([ElementType.TYPE])
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Invariants.class)
@Incubating
@interface Invariant {
Class value()
}Defines class invariants that must hold throughout the object's lifetime. Invariants are checked:
Parameters:
value: Closure class containing the invariant conditionContract Rules:
Usage Examples:
@Invariant({ balance >= 0 })
class BankAccount {
private BigDecimal balance = BigDecimal.ZERO
void withdraw(BigDecimal amount) {
balance -= amount
}
}Multiple Invariants:
@Invariant({ elements != null })
@Invariant({ elements.size() >= 0 })
@Invariant({ capacity > 0 })
class Container {
private List elements = []
private int capacity = 10
}Inheritance Example:
@Invariant({ name != null })
class Person {
String name
}
@Invariant({ employeeId != null })
class Employee extends Person {
String employeeId
// Combined invariant: name != null && employeeId != null
}@Target([ElementType.CONSTRUCTOR, ElementType.METHOD])
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(RequiresConditions.class)
@Incubating
@interface Requires {
Class value()
}Defines method preconditions that must be satisfied by callers. Executed as the first statement in method calls.
Parameters:
value: Closure class containing the preconditionContract Rules:
Usage Examples:
@Requires({ amount > 0 })
void deposit(BigDecimal amount) {
balance += amount
}
@Requires({ divisor != 0 })
@Requires({ dividend != null })
BigDecimal divide(BigDecimal dividend, BigDecimal divisor) {
return dividend.divide(divisor)
}Multiple Arguments:
@Requires({ startIndex >= 0 && endIndex > startIndex && endIndex <= data.size() })
List getSubset(int startIndex, int endIndex) {
return data[startIndex..<endIndex]
}Inheritance Example:
class Shape {
@Requires({ width > 0 })
void setWidth(double width) {
this.width = width
}
}
class Rectangle extends Shape {
@Requires({ width <= maxWidth })
@Override
void setWidth(double width) {
super.setWidth(width)
}
// Combined precondition: width > 0 || width <= maxWidth
}@Target([ElementType.CONSTRUCTOR, ElementType.METHOD])
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(EnsuresConditions.class)
@Incubating
@interface Ensures {
Class value()
}Defines method postconditions that guarantee behavior after method execution. Executed after method completion.
Parameters:
value: Closure class containing the postconditionContract Rules:
result, and old valuesSpecial Variables:
result: Available for non-void methods, contains the return valueold: Map containing previous values of cloneable fieldsUsage Examples:
Basic Postcondition:
@Ensures({ result != null })
String formatName(String firstName, String lastName) {
return "${firstName} ${lastName}".trim()
}Using Result Variable:
@Ensures({ result >= 0 })
@Ensures({ result == old.balance + amount })
BigDecimal deposit(BigDecimal amount) {
balance += amount
return balance
}Using Old Values:
@Ensures({ balance == old.balance - amount })
@Ensures({ old.balance >= amount })
void withdraw(BigDecimal amount) {
balance -= amount
}Constructor Postconditions:
@Ensures({ name != null && name.length() > 0 })
@Ensures({ age >= 0 })
Person(String name, int age) {
this.name = name
this.age = age
}Multiple Postconditions:
@Ensures({ result != null })
@Ensures({ result.size() <= originalList.size() })
@Ensures({ result.every { originalList.contains(it) } })
List<String> filterActive(List<String> originalList) {
return originalList.findAll { isActive(it) }
}@Target([ElementType.TYPE])
@Retention(RetentionPolicy.RUNTIME)
@Incubating
@interface Invariants {
Invariant[] value()
}Container for multiple @Invariant annotations. Automatically used when multiple @Invariant annotations are applied.
@Target([ElementType.CONSTRUCTOR, ElementType.METHOD])
@Retention(RetentionPolicy.RUNTIME)
@Incubating
@interface RequiresConditions {
Requires[] value()
}Container for multiple @Requires annotations. Automatically used when multiple @Requires annotations are applied.
@Target([ElementType.CONSTRUCTOR, ElementType.METHOD])
@Retention(RetentionPolicy.RUNTIME)
@Incubating
@interface EnsuresConditions {
Ensures[] value()
}Container for multiple @Ensures annotations. Automatically used when multiple @Ensures annotations are applied.
old valuesInstall with Tessl CLI
npx tessl i tessl/maven-org-apache-groovy--groovy-contracts