Saltar a contenido

KValid — variante Java (APT)

KSP solo procesa Kotlin. Para clases Java existe kvalid-apt, un annotation processor de javac que reutiliza el mismo dominio (kvalid-core) y el mismo runtime (ValidationResult/Violation/ValidationContext), con un frontend distinto (kspkit-apt: javax.lang.modelkspkit-model) y un emisor JavaPoet.

Instalación

// build.gradle.kts (proyecto Java)
dependencies {
    implementation("io.github.kuroxbyte:kvalid-annotations:0.4.0")
    implementation("io.github.kuroxbyte:kvalid-runtime:0.4.0")
    annotationProcessor("io.github.kuroxbyte:kvalid-apt:0.4.0")
}

Qué genera

Por cada tipo Java @Validated se genera <Type>Validator con:

public static ValidationResult<User> validate(User obj) { ... }

Records (accesor component()) y POJOs (getters). La normalización de tipos Java → canónicos Kotlin hace que las reglas de dominio (escritas contra FQNs Kotlin) apliquen igual a la entrada Java.

Paridad completa con la variante Kotlin

Caso Java (APT)
Constraints built-in ✅ String, numéricos (type-aware), temporales, @NotNull, @OneOf, @Pattern
Cascada @Validated ✅ anidado (address.street)
Custom (@Constraint) ✅ validador Java con constructor sin argumentos
Element-level List<@NotBlank String> vía anotaciones type-use del tipo argumento
Validador de clase ✅ cross-field (ConstraintValidator<TheClass>)

Constraint custom en Java

Un validador reutilizable: una anotación meta-anotada con @Constraint(validatedBy = ...) y una clase Java que implementa ConstraintValidator<T> con constructor sin argumentos (el generado hace new Validator()).

// La anotación de constraint.
@Constraint(validatedBy = SlugValidator.class)
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.RECORD_COMPONENT, ElementType.FIELD, ElementType.TYPE_USE})
public @interface Slug {}

// El validador (constructor sin args).
public final class SlugValidator implements ConstraintValidator<String> {
    @Override
    public void validate(String value, String field, ValidationContext ctx, Map<String, ?> params) {
        if (!value.matches("[a-z0-9-]+")) ctx.violation(field, "slug", Map.of(), null);
    }
}

@Validated public record Article(@Slug String slug) {}

Única asimetría con Kotlin

En Kotlin el validador es un object (singleton) y el generado invoca SlugValidator.validate(...). En Java es una clase y el generado hace new SlugValidator().validate(...), siguiendo el idioma de cada lenguaje (como los ConstraintValidator de Jakarta, instanciados por el proveedor). La interfaz ConstraintValidator<T> y el ValidationContext acumulador son los mismos.

Element-level en Java

Las anotaciones de KValid declaran @Target con TYPE (→ TYPE_USE en Java), así que se aplican sobre el argumento de tipo:

@Validated
public record Post(List<@NotBlank String> tags) {}
// tags[1]=notBlank, tags[2]=notBlank, ...

El frontend APT captura esas anotaciones type-use del mirror del argumento y el dominio las resuelve como constraints de elemento.

Validador de clase (cross-field)

@Constraint(validatedBy = PasswordsMatchValidator.class)
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface PasswordsMatch {}

public final class PasswordsMatchValidator implements ConstraintValidator<Signup> {
    @Override
    public void validate(Signup value, String field, ValidationContext ctx, Map<String, ?> params) {
        if (!value.password().equals(value.confirm()))
            ctx.violation("confirm", "passwordsMatch", Map.of(), null);
    }
}

@Validated @PasswordsMatch
public record Signup(String password, String confirm) {}

Verificación

kvalid-apt trae 6 tests end-to-end (javac + processor): constraints acumulados, cascada, custom, element-level, cross-field y POJOs. Ver los ejemplos ejecutables.