Reducing Validation Boilerplate in Swift with Property Wrappers
Learn how a @Validated property wrapper can centralize email and string checks, cut repetitive code, and what trade‑offs to expect.
22 Jan 2026, 16:59 UTC

The problem: repetitive validation in model initializers
When building Swift models that represent user input, developers often write the same checks—non‑empty strings, email format, numeric ranges—inside every initializer or setter. This leads to verbose code, easy‑to‑miss edge cases, and duplicated logic that is hard to maintain.
Thesis: a property wrapper can centralize the rule
By extracting the validation rule into a reusable @Validated wrapper, the check runs automatically whenever the wrapped property is set, eliminating boilerplate while keeping the validation visible at the point of declaration.
Defining the wrapper
import Foundation
@propertyWrapper
struct Validated<Value> {
private var value: Value
private let predicate: (Value) -> Bool
private let invalidHandler: () -> Value
init(wrappedValue: Value,
_ predicate: @escaping (Value) -> Bool,
default invalidHandler: @escaping () -> Value = { fatalError("Invalid value") }) {
self.predicate = predicate
self.invalidHandler = invalidHandler
// Validate the initial value
guard predicate(wrappedValue) else {
self.value = invalidHandler()
return
}
self.value = wrappedValue
}
var wrappedValue: Value {
get { value }
set {
guard predicate(newValue) else {
value = invalidHandler()
return
}
value = newValue
}
}
}
This wrapper stores the actual value, evaluates a predicate on every assignment, and calls a handler when the predicate fails. The handler can either trigger a precondition failure or return a default value, depending on how you configure it.
Worked example: validating an email address
First, a simple email predicate (for demonstration only):
func isValidEmail(_ string: String) -> Bool {
// Very naive check; replace with a proper regex or DataDetector in real code
return string.contains("@") && string.contains(".") && !string.hasPrefix("@") && !string.hasSuffix(".")
}
Now apply the wrapper to a model:
struct UserProfile {
@Validated("") isValidEmail
var email: String
init(email: String) {
self._email = Validated(wrappedValue: email, isValidEmail) { "" }
}
}
When you create a UserProfile with a valid email, the assignment succeeds. Supplying an invalid string triggers the invalid handler, which in this case returns an empty string.
let good = UserProfile(email: "[contact removed]")
print(good.email) // "[contact removed]"
let bad = UserProfile(email: "not-an-email")
print(bad.email) // "" (empty string because the handler returned a default)
If you prefer a hard failure, replace the default handler with { fatalError("Invalid email") } and the program will stop on invalid input.
Trade‑offs and limitations
- Requires Swift 5.1 or later; older toolchains cannot compile the wrapper syntax.
- Only works on stored variables; you cannot apply @Validated to let constants or computed properties.
- The wrapper adds an extra layer of indirection (a struct with storage) which introduces a tiny runtime cost. In tight loops or performance‑critical code, measure the impact before adopting.
- The wrapper’s initializer validates the initial value, but if you need to defer validation (e.g., async checks) you must extend the pattern or combine it with other techniques.
Actionable next steps
- Copy the @Validated definition into a shared utilities file in your project.
- Define predicates for the rules you need (email, URL, password strength, etc.) and create typed aliases like typealias ValidatedEmail = Validated<String> where the predicate is isValidEmail.
- Replace repetitive validation code in your models with the wrapper, keeping the initializer clean.
- Run a Swift Playground or Xcode project with Swift 5.1+ to confirm that valid assignments pass and invalid ones follow your chosen handler.
- Profile a critical path if you notice performance concerns; the overhead is usually negligible for UI‑bound model updates.
By moving validation logic into a property wrapper, you reduce duplicated code, make the intent explicit at the property declaration, and keep your model initializers focused on assembling state rather than checking it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.