form-doctor
0.0.7indexedAnnotation-based form validation with KSP-powered compile-time metadata, zero-reflection overhead, reactive Compose-ready mutable-state form tracking, field-level error reporting, and extensible custom validators.
Annotation-based form validation with KSP-powered compile-time metadata, zero-reflection overhead, reactive Compose-ready mutable-state form tracking, field-level error reporting, and extensible custom validators.
FormDoc is a powerful Kotlin Multiplatform library designed to simplify form validation. By leveraging KSP (Kotlin Symbol Processing) and Annotations, it automates the generation of validation metadata, allowing you to focus on building great UIs.
Designed with Compose Multiplatform in mind, FormDoc provides a seamless way to manage form states and validation errors across Android, iOS, Desktop, and Web.
mutableStateOf based form tracking.Mark your class with @Validatable and use built-in constraints like @NotBlank, @Email, or @Min.
@Validatable
data class UserProfile(
@NotBlank(message = "Username is required")
val username: String = "",
@Email(message = "Invalid email format")
val email: String = "",
@Min(value = 18, message = "Must be at least 18 years old")
val age: =
)
[!IMPORTANT] Currently, you need to manually create the
FormMetadataRegistryin yourcommonMainsource set. This is because KSP (as used in this project) does not yet support generating code directly into (Or i can't figure a way out😭).
Use rememberFormState to initialize your form. FormDoc uses the KSP-generated UserProfileMetadata to apply the rules, which can be obtained from FormMetadataRegistry.get<UserProfileMetadata>().
@Composable
fun RegistrationForm() {
val formState = rememberFormState(
initial = UserProfile(),
metadata = FormMetadataRegistry.<UserProfile>()
)
FormContent(formState) {
Column {
nameState = state.getState(UserProfile::username)
TextField(
value = nameState.value,
onValueChange = { nameState.value = it },
label = { Text() },
isError = nameState.error != ,
supportingText = { nameState.error?.let { Text(it) } }
)
Button(onClick = {
state.submit(
onValid = { -> println() },
onInvalid = { errors -> println() }
)
}) {
Text()
}
}
}
}
Need something specific? Implement FieldValidator and use @ValidatedBy.
class PasswordValidator : FieldValidator<String> {
override fun validate: String? {
(value.length < )
}
}
(
password: String =
)
Add the KSP plugin and dependencies to your build.gradle.kts:
plugins {
id("com.google.devtools.ksp") version "x.x.x"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.deference3:formdoc:0.0.2")
// other configurations
}
// other configurations
}
// other configurations
}
dependencies {
ksp("io.github:formdoc-processor:0.0.2")
}
This project is licensed under the MIT License - see the LICENSE file for details.
Built with ❤️ for the Kotlin Multiplatform community.
commonMainCreate a file at src/commonMain/kotlin/formdoc/registry/FormMetadataRegistry.kt with the following content:
package formdoc.registry
import me.deference.formdoc.FormMetadata
expect object FormMetadataRegistry {
inline fun <reified T : Any> get(): FormMetadata<T>?
}
Once KSP support (Or my skill issue is solved🥲) for commonMain generation is improved, this manual step will be removed.
@Validatable.FormProcessor scans these annotations.*Metadata class (e.g., UserMetadata) that implements FormMetadata. This class contains a map of properties to their respective validators.FormMetadataRegistry.get<*YourFormModel*>() (e.g., FormMetadataRegistry.get<UserProfile>()).FormState uses this metadata to perform validation whenever validateAll() or submit() is called, updating the FieldState reactively.Surfaced from shared tags and platforms — no rankings paid for.