XCEValidatableValue defines reusable value specifications and applies them to editable model properties with Swift property wrappers. Raw input stays easy to edit, while validated and converted values are available only through an explicit throwing API.
It keeps domain-specific validation rules centralized, declarative, and reusable instead of scattering checks throughout an application.
Version 6 is a new major release with a deliberately redesigned API. See Migrating from 5.x before updating an existing project.
- Swift 6.0 or later
- macOS 12+, Mac Catalyst 15+, iOS 15+, tvOS 15+, watchOS 8+, or visionOS 1+
Add the package to Package.swift:
dependencies: [
.package(
url: "https://github.com/XCEssentials/XCEValidatableValue",
from: "6.0.0"
)
]Then add the product to your target:
.target(
name: "YourTarget",
dependencies: [
.product(
name: "XCEValidatableValue",
package: "XCEValidatableValue"
)
]
)A ValueSpecification describes the raw editable type, the validated output type, field metadata, and conditions evaluated before and after conversion.
import XCERequirement
import XCEValidatableValue
enum Email: ValueSpecification {
typealias Raw = String
static let field = ValidationField(
name: "Email",
placeholder: "name@example.com",
hint: "Used to sign in"
)
static let conditionsOnRaw: [Condition<String, Never>] = [
Condition("Contains @") { $0.contains("@") }
]
}Declaring a specification as an enum creates a non-instantiable namespace for its static validation rules.
When Raw and Valid are the same type, conversion is supplied automatically. Collections are considered empty when their isEmpty property is true; other types are non-empty by default. Override isEmpty(_:) when the domain needs different behavior.
Declare Valid and a typed ConversionFailure when validated output has a different type:
enum Age: ValueSpecification {
typealias Raw = String
typealias Valid = Int
enum ConversionFailure: Error, Sendable {
case notAnInteger
}
static let conditionsOnValid: [Condition<Int, Never>] = [
Condition("At least eighteen") { $0 >= 18 }
]
static func convert(_ rawValue: String) throws(ConversionFailure) -> Int {
guard let age = Int(rawValue) else { throw .notAnInteger }
return age
}
}If Valid conforms to RawRepresentable and its RawValue matches Raw, conversion is also supplied automatically.
Use Required and NonRequired as property wrappers. Both expose Raw? as the wrapped property so a draft model can represent missing input.
struct RegistrationForm: ValidatableEntity, Codable {
@Required<Email> var email: String? = nil
@Required<Age> var age: String? = nil
@NonRequired<Nickname> var nickname: String? = nil
}
enum Nickname: ValueSpecification {
typealias Raw = String
static let conditionsOnRaw: [Condition<String, Never>] = [
Condition("At most thirty characters") { $0.count <= 30 }
]
}Edit the raw values through the ordinary properties:
var form = RegistrationForm()
form.email = "ada@example.com"
form.age = "37"
form.nickname = "Countess"Validate the entire entity, then obtain converted values from the projected wrappers:
try form.validate()
let email: String = try form.$email.validatedValue()
let age: Int = try form.$age.validatedValue()
let nickname: String? = try form.$nickname.validatedValue()Requiredness has a direct effect on validation and return types:
| Wrapper | Missing or empty input | validatedValue() |
|---|---|---|
Required<Spec> |
Produces a presence issue | Returns Spec.Valid or throws |
NonRequired<Spec> |
Represents valid absence | Returns Spec.Valid? or throws |
Wrappers encode and decode as their raw single value rather than as an implementation-specific container.
Every non-empty value passes through the same ordered pipeline:
conditionsOnRawchecks editable input.convert(_:)creates the valid representation.conditionsOnValidchecks the converted value.
Validation stops before conversion when a raw condition fails, and stops before valid-value conditions when conversion fails. All failed conditions in the active phase are reported together.
Call validationIssues() when errors should be displayed without throwing:
for issue in form.validationIssues() {
print(issue.field.name)
print(issue.phase)
print(issue.message)
}validate() and validatedValue() throw ValidationError, whose issues property contains the same [ValidationIssue]. Each issue includes:
- its
ValidationField; - the
ValidationPhase(presence,rawValue,conversion, orvalidValue); - a displayable message and optional requirement description;
- the
RequirementContextcall site.
Diagnostics and errors conform to Codable, Hashable, and Sendable.
ValidatableEntity automatically discovers stored Validatable members, including property-wrapper storage, nested entities, and stored ValidationPlan values. Issues retain declaration order.
Use a plan to compose explicit validation:
let shouldValidateNickname = true
let plan = ValidationPlan {
form.$email
form.$age
if shouldValidateNickname {
form.$nickname
}
}
try plan.validate()An entity can implement additionalValidation for rules not represented by stored members, such as a cross-field comparison. The result builder accepts individual validatable values, conditionals, and loops.
Mark a specification as SecretValue to prevent conversion error details from exposing sensitive input:
enum Password: ValueSpecification, SecretValue {
typealias Raw = String
static let conditionsOnRaw: [Condition<String, Never>] = [
Condition("At least eight characters") { $0.count >= 8 }
]
}The wrapper's metadata.isSecret value is also true, allowing a UI to select secure input controls.
Version 6 intentionally removes the legacy API rather than carrying deprecated aliases. The principal migrations are:
| 5.x | 6.x |
|---|---|
SomeValidatableValue |
ValueSpecification |
SomeValidatable |
Validatable |
SomeValidatableEntity |
ValidatableEntity |
IsSecretValue |
SecretValue |
DisplayNamed / DisplayNamedInfo |
ValidationField |
Optional Required<Spec>? wrapper storage |
@Required<Spec> var value: Spec.Raw? |
Optional NonRequired<Spec>? wrapper storage |
@NonRequired<Spec> var value: Spec.Raw? |
rawValue on a wrapper |
The wrapped property |
validValue or vv |
$property.validatedValue() |
convert(rawValue:) -> Valid? |
convert(_:) throws(ConversionFailure) -> Valid |
Case-based ValidationError |
ValidationError.issues with structured phases |
ValidatationMetadata |
ValidationMetadata |
The 5.x convenience protocols and global helpers for Boolean flags, checkmarks, email checks, wrapper construction, optionals, and wrapper comparison are no longer part of the package. Model those behaviors explicitly in a ValueSpecification or in application code.
Run the test suite with:
swift testThe project is licensed under the MIT License.