Kotlin DSL

Use krandom-kotlin-dsl when Kotlin tests need compact object-generation rules without dropping into Java builders.

Dependency

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("io.github.frikit:krandom-kotlin-dsl:2.10.0")
}

Generate One Object

import io.github.frikit.krandom.dsl.krandom

val user = krandom<UserDto> {
    config { seed(42L) }
    rule(UserDto::firstName) { "Ada" }
    rule(UserDto::email) { "ada@example.test" }
}

Typed Rules and Validation

Prefer property references over string names: they survive renames, and the compiler checks that the rule value matches the property type. Use the string form only for fields that cannot be referenced as Kotlin properties.

Rules are validated before generation:

  • registering two rules for the same field or the same type fails immediately;
  • a rule or exclusion naming a field that does not exist on the target class fails when the generator is built, listing the known field names;
  • a rule or exclusion on a computed (val x get() = ...) or delegated (by lazy) property fails when the generator is built: such properties have no backing field and are not primary-constructor parameters, so nothing could apply to them.
val account = krandom<Account> {
    rule(Account::owner) { "grace" }   // type-safe; survives refactoring
    exclude(Account::internalNotes)    // type-safe exclusion
}

Field rules and exclusions are scoped to the generated (root) type (2.6+): exclude(Customer::name) leaves Customer.name at its initial value while a nested Company.name is still generated. A field inherited from a superclass is matched through that superclass field, so it also applies to other subclasses of it in the same object graph. Use ruleForType for type-wide rules and the Java ObjectFaker for nested-path rules. Exclusions on immutable data classes built through the primary constructor are not yet honoured by the constructor adapter; exclude those fields on mutable types or register a rule instead.

Generate Lists

import io.github.frikit.krandom.dsl.krandomList

val users = krandomList<UserDto>(10) {
    rule("country") { "United States" }
}

Reusable Generators

import io.github.frikit.krandom.dsl.krandomGenerator

val users = krandomGenerator<UserDto> {
    ruleForType<String> { "fixed" }
}

val one = users.generate()
val many = users.generateList(5)

Rules are backed by GeneratorConfig object overrides, so the DSL shares the same defaults, seeding, locale, and object-generation behavior as the Java API.

That includes unique fields. A rule or ruleForType may repeat a constant on every object, as above. To request a field whose values must not repeat, use config { objectUniqueFields(...) }, which replaces the names that are unique by default. A constant on a requested field fails on a second object from the same generator.

Java fixture API and Kotlin DSL scope

The Kotlin DSL is the concise, typed front end for ordinary root-property fixtures. It deliberately does not claim every Java ObjectFaker feature. Choose the Java fixture API when a test needs the broader rule-composition surface.

Capability Java ObjectFaker Kotlin DSL
Root property rules String, accessor, and PropertyPath rules Property-reference and string rules
Nested-path rules Supported Use Java ObjectFaker
Contextual and dependent rules Supported Use Java ObjectFaker
Include, profiles, post-processing, and populate Supported Use Java ObjectFaker
Immutable reusable models ObjectModel<T> Use Java ObjectModel
Kotlin primary constructors Via the Kotlin DSL module Via the Kotlin DSL module
Initialized properties A field rule replaces them; other values keep them unless objectOverrideDefaultInitialization(true) Rules, ruleForType values, and generated values all replace them

In both APIs, an explicit field rule takes precedence over a type rule and generated defaults. The DSL validates duplicate field/type rules immediately (a Java ObjectFaker rule instead replaces an earlier rule for the same path) and unknown root field names when building. It supports neither value classes nor sealed/abstract types without a concrete type override. These boundaries are intentional: they keep the Kotlin adapter small while the Java API remains the single place for complex fixture composition.

Immutable Kotlin classes

krandom-kotlin-dsl includes Kotlin reflection and constructs immutable Kotlin values through their primary constructor. No extra kotlin-reflect declaration is needed.

data class UserFixture(
    val name: String,
    val email: String,
    val roles: List<Role>,
    val source: String = "test"
)

data class Role(val code: String)

val user = krandom<UserFixture> {
    rule("name") { "Ada Lovelace" }
    rule("email") { "ada@example.test" }
}

check(user.source == "test") // optional parameters retain their Kotlin default

The primary-constructor adapter uses the normal kRandom resolver for each parameter, so field/type rules, nested generic containers, semantic values, deterministic seeds, and Jakarta Validation annotations all apply. Both @param: and @field: constraint targets are recognized. An explicit field or type rule wins over an optional parameter default.

Kotlin object singletons return their one instance. Value classes and sealed/abstract Kotlin types are rejected before returning a value; use a concrete type or explicit type override. A core-only application also rejects immutable Kotlin types with a message naming krandom-kotlin-dsl, rather than allocating an invalid instance.

The adapter always invokes the primary constructor; secondary constructors are never selected. Delegated properties initialize normally as part of that construction. A required recursive immutable graph fails contextually rather than returning a value with a runtime null.

Immutable Data Classes, Generics, Defaults, and Constraints

// Immutable data classes construct through the primary constructor
data class Account(val owner: String, val balance: Long)
val account = krandom<Account> { rule(Account::owner) { "grace" } }

// Nested generics keep their declared element types
data class Ledger(val entries: List<Map<String, Long>>)
val ledger = krandom<Ledger>()

// Kotlin default parameter values are honored unless a rule overrides them
data class Invoice(val currency: String = "EUR", val amount: Long)
val invoice = krandom<Invoice>()          // currency == "EUR"

// Jakarta Validation constraints on constructor parameters are satisfied
data class Customer(@field:jakarta.validation.constraints.Email val contact: String)
val customer = krandom<Customer>()

Standalone Configuration and Replay Recipes

val config = krandomConfig {
    seed("checkout-fixtures")   // textual seeds use the shared derivation contract
    constructionPolicy(ObjectConstructionPolicy.SAFE)
}
val recipe = config.generationRecipe.orElseThrow().serialize()  // portable replay recipe

The DSL matches the Java builder defaults with one intentional, documented difference: objectOverrideDefaultInitialization is enabled, so ruleForType values and generated values also replace property initializers. A rule(...) replaces its property’s initializer, as field rules do in the Java API.

Opt-in independent streams (2.3+)

import io.github.frikit.krandom.generator.`object`.ObjectFieldStreamPolicy

val users = krandomList<User>(5) {
    config {
        seed(42L)
        objectFieldStreamPolicy(ObjectFieldStreamPolicy.INDEPENDENT)
    }
    rule(User::name) { "Ada" }
}

The Kotlin option uses the same core policy as Java. Existing configurations remain LEGACY. For direct configuration consumers, krandomConfig { seed(42L) }.snapshotClock() captures an explicit replay clock.