How do you enforce that an Email or Percentage value class only ever holds a valid value, and what are the limits of doing so?
answer
- init + require rejects bad input at construction
- val + no other state => stays valid
- init available since 1.4.30
- Deserialization/reflection can bypass init
- Validate untrusted input at the boundary
basics
~10 sPut a check in the value class's init block so construction fails for bad input. But it can't guarantee validity if the value is created by deserialization or unsafe casts that bypass the constructor.
solid answer
~40 sAdd an `init` block to the value class (supported since Kotlin 1.4.30) and call `require(...)` to reject invalid input at construction — e.g. `require(value in 0.0..100.0)` for a Percentage, or an email-format check. Because the only stored value is `val` and immutable, once constructed validly it stays valid. The limit: validation only runs through the normal constructor. Paths that bypass it — some reflection/serialization frameworks, JVM interop, or unsafe casts — can produce an instance whose underlying value was never checked. So treat `init` validation as a strong guard for in-language construction, but for untrusted/deserialized input, validate at the boundary (e.g. a factory like `Email.parse(): Result<Email>`) rather than relying solely on `init`.
go deeper
Knows to put a require check in an init block to reject invalid values.
Explains immutability means a valid instance stays valid and that init has been available since 1.4.30.
Identifies bypass paths (deserialization/reflection) and adds boundary validation or a safe factory.
Designs a codebase-wide policy: typed scalars validated once at boundaries, serialization configured to honor constructors, parse-don't-validate enforced.
## Validating in the constructor Since Kotlin **1.4.30**, a value class may have an **`init` block** — code that runs when an instance is constructed. Combine it with `require` (throws `IllegalArgumentException` when the condition is false) to reject bad values: ```kotlin @JvmInline value class Percentage(val value: Double) { init { require(value in 0.0..100.0) { "out of range: $value" } } } @JvmInline value class Email(val value: String) { init { require(EMAIL_REGEX.matches(value)) { "bad email: $value" } } } ``` Because the single property is a `val` (immutable) and the class has no other stored state, a successfully constructed instance is **always valid for its lifetime** — there's no setter to corrupt it later. This is the 'parse, don't validate' / 'make illegal states unrepresentable' pattern: once you hold a `Percentage`, you don't re-check the range. ## The limits `init` only runs on the **normal construction path**. Some routes can bypass it: - **Deserialization / reflection**: certain frameworks construct objects without running constructors, or set the underlying field directly. - **Platform/interop edges**: Java callers or generic code may hand you an unchecked underlying value. - **Unsafe casts**: forcing a raw value into the type via interop tricks. When the underlying value comes from such a path, it may never have been validated. ## Practical guidance - Use `init` + `require` as the **primary** in-language guard — it's cheap and authoritative for Kotlin construction. - For **untrusted input** (HTTP bodies, files, external systems), validate explicitly at the boundary, e.g. a total/safe factory: ```kotlin @JvmInline value class Email private constructor(val value: String) { companion object { fun parse(raw: String): Result<Email> = if (EMAIL_REGEX.matches(raw)) Result.success(Email(raw)) else Result.failure(IllegalArgumentException("bad email")) } init { require(EMAIL_REGEX.matches(value)) } } ``` - Configure serialization frameworks to route through the validating constructor where possible. ## Keywords `init` block, `require`, `IllegalArgumentException`, immutability (`val`), parse-don't-validate, private constructor + factory, deserialization bypass.
- Once a Percentage is constructed, do callers need to re-check its range?No. The value is an immutable `val` and was validated in `init`, so any in-language-constructed Percentage is guaranteed in range — that's the point of parse-don't-validate.
- Why can't init validation be fully trusted for JSON input?Some deserializers bypass the constructor or set the field directly, so the value may never reach `init`. Validate at the deserialization boundary or route the framework through the validating constructor/factory.
init validation is the bouncer at the front door; deserialization is a side window — guard the boundary, not just the door.
saying these in an interview costs you the question
- Claiming init runs on every possible construction path including reflection
- Saying value classes can't have init blocks at all
- Forgetting the property is immutable, so re-validation is needless in-language
- Trusting init alone for untrusted external input
- Using check() for argument validation instead of require()