Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Service SDK: ModelConfigOverrides records are forced to declare an all-null no-arg constructor #2493

Copy link
Copy link

Description

@larsbeck
Issue body actions

Context

While addressing review feedback on TimefoldAI/timefold-quickstarts#1112 (conference-scheduling conversion to the Models Service SDK), this constructor had to stay in the model's ModelConfigOverrides record:

public ConferenceScheduleConfigOverrides() {
    this(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
            null, null);
}

Review comment (rightly): "One of those ridiculous constructors. This code does not tell the reader anything. What does the 15th null do?" — TimefoldAI/timefold-quickstarts#1112 (comment)

Why the model can't avoid it

DefaultConfigProfileProcessor (service quarkus deployment module) instantiates the model's ModelConfigOverrides type reflectively via getDeclaredConstructor().newInstance(), i.e. it hard-requires a no-arg constructor. For a Java record that requirement is inherently ugly:

  • Every record has a canonical constructor covering all components; a ConfigOverrides for a model with 20 constraints has 20 components.
  • Every secondary constructor must delegate to the canonical one (JLS 8.10.4), so a no-arg constructor is forced to spell out one null per component.
  • The canonical constructor can't be hidden either: it must be at least as accessible as the record class, and these records are public (REST/Jackson/OpenAPI).

So every converted model carries an N-null constructor whose only purpose is to satisfy the SDK's reflection, and N grows with the constraint count.

Suggested fix

Records carry everything needed to construct an "empty" instance without a no-arg constructor. In DefaultConfigProfileProcessor (and anywhere else the SDK reflectively instantiates user config types):

static <T> T emptyInstance(Class<T> type) throws ReflectiveOperationException {
    if (type.isRecord()) {
        var components = type.getRecordComponents();
        var paramTypes = Arrays.stream(components).map(RecordComponent::getType).toArray(Class<?>[]::new);
        var args = Arrays.stream(components)
                .map(c -> defaultValueFor(c.getType())) // null for references, 0/false for primitives
                .toArray();
        return type.getDeclaredConstructor(paramTypes).newInstance(args);
    }
    return type.getDeclaredConstructor().newInstance();
}

This keeps backwards compatibility (classes still use the no-arg constructor), and models can then delete the null-litany constructor entirely. A compact constructor that applies defaults/normalization still runs, since the canonical constructor is invoked.

Why not "just use a class instead of a record"

Records are the right vehicle for these DTOs and the SDK conventions already lean on that: immutability by construction, a single canonical construction path whose compact constructor centralizes normalization/validation, component-driven schema generation (@ConstraintReference/@Schema on components), and free equals/hashCode/toString. Replacing a 20-component record with a class means 20 fields + 20 accessors + hand-written plumbing — strictly more of exactly the boilerplate the quickstarts review asked to remove — and a mutable overrides object shared through profile merging is a footgun. So the class route is not a good trade.

That said, if there's a better way to solve this than the record-aware instantiation sketched above, ideas are very welcome.

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.