Migration

To 4.0

4.0 replaces the generated mutable RW object with a true Builder and materializes a completed, structurally immutable DSL Object graph before validation. Completed models no longer expose generated apply, owned composition cannot adopt already completed objects, lifecycle extensions are split at the new INSTANTIATE phase, and collection declarations now have explicit snapshot-safe limits. Templates now have persistent graph-wide recipe identity separate from ordinary models; marked Templates cannot be relationship values or ordinary Jackson export values, and deferred Builder actions cannot be scheduled at phase 40 or later. Jackson is an asymmetric external-format integration rather than Klum persistence and adds no wire metadata.

See the dedicated Builder First Migration guide for the complete migration checklist and compatibility breaks, plus Templates, Copy Strategies, and Model Phases for materialization boundaries and Jackson Integration for foreign-data import and ordinary POJO export.

Validation callers must import and catch com.blackbuild.klum.ast.runtime.validation.KlumValidationException. The former com.blackbuild.klum.ast.runtime.KlumValidationException type has been removed; this is an intentional 4.0 source and binary compatibility break.

Template creation and scoped application

Create a reusable Template below Create, then apply it below Template:

(See: TemplatesDocumentaryTest#'applies one scoped template to multiple service configurations'.)

def baseline = ServiceConfiguration.Create.Template.With(region: 'eu-central')

ServiceConfiguration.Template.With(baseline) {
    ServiceConfiguration.Create.With { }
}

Foo.Create.Template(...) and Foo.Create.TemplateFrom(...) have been removed so Foo.Create.Template can be the unambiguous generated Factory property. Migrate them to Foo.Create.Template.With(...) and Foo.Create.Template.From(...). The older documented Foo.Template.Create(...) and CreateFrom(...) spellings remain deprecated 4.x aliases; move new code to the canonical Create.Template chain. The best-effort Builder First migration helper recognizes only direct type-qualified occurrences. Run it in a clean, version-controlled schema-module worktree, inspect its diff, then compile and finish the checklist.

Code that explicitly named the generated scoped-application type must now use Foo_DSL.TemplateScope rather than Foo_DSL.Template. This intentional 4.0 RC source and binary compatibility break has no alias: recompile schemas and typed Java or static Groovy clients after changing the name. Foo_DSL.Factory.Template remains the distinct type for the literal Foo.Create.Template root-creation field.

For a foreign YAML/JSON migration, configure one caller-owned Jackson mapper, import one input into one Builder lifecycle, and treat the completed-model export as a separately owned external projection. Do not feed it back as Klum persistence or use repeated imports as a Jackson-specific merge/layering mechanism; #304 owns source-neutral composition.

Named modules and Groovy

Groovy 3 remains an ordinary-classpath configuration. Do not add a KlumAST module-info.java to a Groovy 3 schema or compensate with JVM module flags.

Groovy 4 and 5 schemas may be named modules and must require org.apache.groovy, the annotations/runtime modules, and requires static com.blackbuild.klum.ast.compiler; add adapter modules only when used. A schema that uses Jackson and Jakarta Bean Validation has this narrow descriptor shape:

module example.schema {
    requires com.blackbuild.klum.ast.annotations;
    requires com.blackbuild.klum.ast.runtime;
    requires static com.blackbuild.klum.ast.compiler;
    requires com.blackbuild.klum.ast.jackson;
    requires com.blackbuild.klum.ast.validation.bean;
    requires org.apache.groovy;

    exports example.schema;
    opens example.schema to com.blackbuild.klum.ast.runtime,
        com.fasterxml.jackson.databind,
        org.hibernate.validator;
}

Keep the runtime opening for generated Builder lifecycle/materialization. Add the Jackson and Hibernate Validator targets only when those adapters inspect the schema; do not substitute --add-reads, --add-exports, or --patch-module flags.

The Schema plugin validates this user-owned descriptor through validateKlumSchemaModule, which is part of check and Maven publication. It reports copyable missing requires and qualified opens directives but never rewrites module-info.java. The generated Foo_DSL mirrors used for IntelliJ completion are not module sources, compiler inputs, or publication inputs.

Recompile schemas and clients for 4.0. Java-serialized 3.x graphs are not 4.0 migration inputs, and Java serialization is not a cross-version persistence format; own external compatibility data and migrations in the Schema.

Custom validation reporting

Validator and ValidatorBase are removed in 4.0; there is no source or binary compatibility adapter. In lifecycle methods, lifecycle closures, and validation classes, replace their shortcuts with KlumSchemaSupport.getKlumValidation() (Groovy's klumValidation property) and its error, errorAt, issue, issueAt, suppressOn, or suppressAll operations. Use KlumSchemaSupport.klumValidationForObject(target) only when a lifecycle callback must report an issue on an explicit object. It retains that object's construction path and does not alter lifecycle ordering. The separate #406 compiler restriction remains a later placement check; it is not a migration fallback.

KlumCast 0.4 final dependencies

KlumAST 4.0 uses the immutable KlumCast 0.4.0 artifact set: klum-cast-annotations, klum-cast-spi, and klum-cast-compile. Its stable automatic module names are com.blackbuild.klum.cast.annotations, com.blackbuild.klum.cast.spi, and com.blackbuild.klum.cast.compiler; do not substitute filename-derived names or use local module-path flags to compensate for an invalid dependency graph.

Recompile schemas and custom checks when moving to KlumAST 4.0. KlumAST's built-in name-bound checks use KlumCast's durable stateless Check SPI and report structured, source-positioned diagnostics. Custom checks must implement that SPI; the deprecated compatibility adapter is only a temporary migration aid for external consumers (#460).

To 2.2

toString() methods are not automatically generated anymore, to restore the old behavior, add the @ToString annotation to the classes. manualValidation has been dropped, as it does not work with stackable issues. This feature can be simulated by either downgrading the issues on the object at hand or skipping the Verify phase and handling errors manually

To 2.1

It is strongly advised to first update to 2.0 and the to 2.1.

2.1 drops all deprecated methods of 2.0. Since they are documented, replacing them with their new counterparts should be straightforward.

To 2.0

The sections below describe historical migration steps and may show APIs, such as completed-model apply, that were subsequently removed in 4.0. Apply the historical migration first, then follow Builder First Migration.

Validation now throws KlumValidationException

which wraps KlumValidationResults for the validated objects, each containing the relevant KlumValidationIssues. Previously, an AssertionError was thrown, so calling code might need to be adjusted.

Multiple Inner Create Calls on the Same Field (or Key in a Map Field) Now Stack Instead of Replacing

Previously, multiple calls to the same inner create method would replace the previous value.

Foo.Create.With {
    bar {
        value = 1
        anotherValue = 2
    }
    bar {
        anotherValue = 3
    }
}

Previously, the second call of "bar" would result in a new object, i.e. the object created by the first call would be replaced, resulting in:

foo.bar.anotherValue == 3
foo.bar.value == null

Now those calls stack, so the result would be:

foo.bar.anotherValue == 3
foo.bar.value == 1

This is especially useful for using deep templates to set the first object:

def template = Foo.Create.Template {
    bar {
        value = 1
        anotherValue = 2
    }
}

Foo.withTemplate(template) {
  Foo.Create.With {
      bar {
          anotherValue = 3
      }
  }
}

If the existing object does not match the new object (either because a different key is provided or specific type is given that is different from the existing type), an Exception is thrown. In that case, the behaviour can be explicitly overridden by using either apply or a setter (to explicitly merge or replace):

Foo.withTemplate(template) {
  Foo.Create.With {
      bar.apply { // explicitly force merge
          anotherValue = 3
      }
  }
}

or

Foo.withTemplate(template) {
  Foo.Create.With {
      bar = Bar.Create.With { // explicitly force overwrite
          anotherValue = 3
      }
  }
}

Default Values are actually set, not only returned

This makes objects used in copyFrom behave differently. Previously, the copyFrom methode explicitly ignored default values, now they would be copied as well if already set. This can lead to different results if the copy source a) is not a template object and b) was created outside the current phase run.

If the object was created outside the phase run, it will most likely be a template, so using the Create.Template() creator method (or the deprecated createAsTemplate() method) will lead to the same result as before.

Owners are now set in the owner phase

Previously, they have been set before apply was called, so apply had already access to the owner, which could be used in separate scripts. Any logic accessing the owner must be placed in a later phase (for example AutoLink or PostTree).

This also holds true for methods using default values populated by the owner.

PostApply methods accessing the owner must also be move to a later phase (or split).

Layer 3 @LinkTo relationships

In 4.0, @LinkTo is an OPTIONAL_LINK relationship by default. A local Builder created in the current construction session remains owned composition; an already owned Builder or a completed DSL Object is an aggregation target. This preserves the usual Layer 3 pattern in which a local value overrides an Auto-Link fallback.

Use @Field(FieldType.LINK) @LinkTo when the field must be aggregation-only. Ordinary relationships remain composition-only and reject completed or already claimed Builders. Custom @AutoLink code that previously overwrote a configured value must use builder.link(fieldName, target) for an explicit non-destructive fallback instead.

Deprecation: Validation annotation -> Validate

@Validation.mode() is replaced by phases and thus ignored. Validation.Option.IGNORE_UNMARKED is default anyway, so the only useful variation of the annotation is @Validation(option=VALIDATE_UNMARKED), which is replaced by @Validate on class level.

Deprecation: Factory methods -> Factory class

All static factory methods on DSL classes are deprecated in favor of a single Create class field which encapsulates all relevant factory methods.

The following factory calls should be renamed:

Old New
Foo.create() Foo.Create.One()
Foo.create(...) Foo.Create.With(...)
Foo.createFrom(...) Foo.Create.From()
Foo.createAsTemplate(...) Foo.Create.Template(...)
Foo.createFromClasspath(...) Foo.Create.FromClasspath(...)

NOTE that in addition to .One() for empty factory calls, .With() is also working, but since it makes for a strange sounding call is deprecated and only present to allow a simple search and replace.

Dependency changes

For 2.0, the single klum-ast dependency is replaced by two KlumAST is split into three distinct jars:

klum-ast-annotations

Does not usually need to be addressed directly except in very special cases, since it is a dependency of both of the other jars.

klum-ast

Contains the actual AST transformations, i.e. the core of KlumAST. These need to be present during compile-time only and need not be present on runtime (usually it should be safe if they are).

klum-ast-runtime

Contains classes needed during runtime.

compileOnly vs. Runtime Scope

Since klum-ast now relies on a runtime component, a schema now should have two separate dependencies, klum-ast as compileOnly (provided for Maven) and and klum-ast-runtime as api (runtime for Maven), i.e.:

dependencies {
  compileOnly 'com.blackbuild.klum.ast:klum-ast:4.0.0'
  implementation 'com.blackbuild.klum.ast:klum-ast-runtime:4.0.0'
}

or

<dependencies>
  <dependency>
    <groupId>com.blackbuild.klum.ast</groupId>
    <artifactId>klum-ast</artifactId>
    <version>...</version>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>com.blackbuild.klum.ast</groupId>
    <artifactId>klum-ast-runtime</artifactId>
    <version>...</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

To 1.2

DelegateOnly Strategy for closures

Closures are all DelegateOnly instead of the previous DelegateFirst. This means that you cannot access methods of an outer object directly (which would not be very intuitive). If you need this functionality, you need to access the outer object directly, using the owner property of Closure, an @Owner field of the outer instance or a local variable pointing to the targeted instance.

Instead of:

Foo.create {
    bar {
        methodInFoo()
    }
}

Write instead:

Foo.create {
    bar {
        owner.methodInFoo() // owner is property of Closure
    }
}

Note that naming an @Owner field actually owner leads to the field being overshadowed by the owner field of the closure. While this is usually not a problem, it might cause failures when used inside a Collection-Factory:

@DSL
class Bar {
  @Owner
  Foo owner
}

Foo.create {
    bars {
        bar {
            // owner points to the owner of the closure, i.e. the collection factory, so this will fail:
            owner.doSomething()
        }
    }
}

Calling the owner field (or any other field) actually owner leads to a compiler warning (which might eventually be replace with a compiler error). Consider using a more domain specific field name like graph or parent is advisable.

Breaking changes since 0.98

Breaking changes since 0.17

the following features were dropped: