immutables / immutables

Annotation processor to create immutable objects and builders. Feels like Guava's immutable collections but for regular value objects. JSON, Jackson, Gson, JAX-RS integrations included
http://immutables.org
Apache License 2.0
3.44k stars 274 forks source link

Support for java records #1285

Open lazystone opened 3 years ago

lazystone commented 3 years ago

Now with Java 16 we finally have java records which are native data holders, but what java records lack is the Builder functionality.

It would be nice to add support for Java records, so it's possible to generate Builders for existing java records(as it's done today for interfaces).

elucash commented 3 years ago

Thank you for the feature request. I've seen something like this was already opened and then closed as something already working (to some degree). Can you please check if the following solution works:

  1. add org.immutables:builder artifact dependency (annotations only)
  2. Put @Builder.Constructor annotation on a record. This annotation would then propagate to record constructor (because of @Target(ElementType.CONSTRUCTOR)) and the builder would be, hopefully generated in the same package as it does for Pojos: https://immutables.github.io/factory.html#pojo-constructors
lazystone commented 3 years ago

It kind of works actually, but @Builder.Constructor has @Target(ElementType.CONSTRUCTOR), so instead of

@Builder
public record MilestoneDumpRecord(
    UUID id, String status, @JsonInclude(Include.NON_ABSENT) Optional<String> text) {}

I have to write

public record MilestoneDumpRecord(
    UUID id, String status, @JsonInclude(Include.NON_ABSENT) Optional<String> text) {

  @Builder.Constructor
  public MilestoneDumpRecord(
      UUID id, String status, @JsonInclude(Include.NON_ABSENT) Optional<String> text) {
    this.id = id;
    this.status = status;
    this.text = text;
  }
}

Which is kind of nullifies benefits of records. It would be nice just to have some convenience annotation which can be put on the class itself(like @Builder.Record maybe?)

lazystone commented 3 years ago

However in many cases I probably have to write constructor anyway - to make Optional and List fields non-null...

lazystone commented 3 years ago

Yeah... I realized that in order to imitate some of the Immutables behavior for java records I have to write constructors for each record class(default values, checking for nulls), so for me @Builder.Constructor works as it is. Closing this.

Thanks for your help @elucash!

cykl commented 3 years ago

Something we are loosing with Java records are wither methods. Being able to create an immutable object from scratch using a builder is nice, but if I cannot easily create a new instance derived from an original one it kind of defeat usefulness of whole thing (i.e. if handwritten derivation is bearable then regular creation is likely fine too).

I just stumbled upon a project that seems to be able to enhance record with wither methods (https://github.com/Randgalt/record-builder#Wither-Example).

elucash commented 3 years ago

Ah, default methods... right, we can plug implementation there. We already generate wither interface only containing wither signatures while implementation class is expected to implement those. But for records we need to rework those part of functionality a bit. We definitely need some record support beyond just workaround (see comments above) and hopefully be able to use most or at least fraction of other functionality we already provide for the interface/abstract class-based immutable objects, obviously excluding some incompatible features (lazy, derived attributes come to mind, like if want those - should be ok to switch from a record to an interface/abstract class for a value type).

cykl commented 3 years ago

This is likely only a temporary need as a future version of Java shouls bring this feature to records, see https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md#withers. However, I haven't seen public sign of progress since August 2020 (deconstruction and pattern matching being "slowly" added releases after releases) and think it's safe to say it won't be available in next LTS.

elucash commented 3 years ago

BTW, just tried myself, it seems that there are no need to specify all constructor parameters just to annotate constructor. So called "compact" constructor can be used

record Uio(
  int x,
  String b,
  boolean z
) {
  @Builder.Constructor Uio {}
}

var b = new UioBuilder()
  .x(4)
  .b("xx")
  .z(true)
  .build();
KangoV commented 3 years ago

I'm really interested in this. Will it be coming?

elucash commented 3 years ago

Asides from what is already working right now (see above), i think we can add some minor functionality like Immutable annotation on top (instead of compact constructor) and generated super-interface with default with*methods. But that would be only after Java LTS release, that should be soon IIRC

dhoepelman commented 3 years ago

As a first step, I have a version allowing @Builder.Constructor on the record level, I will try to open a PR soon.

Question to the maintainers, currently all record-level annotations are skipped, with an Unmatched annotation will be skipped for annotation processing warning.

For implementing record support, do you want to follow a. opt-in approach: explicitly enable annotations that make sense for records or b. opt-out: enable all current class-level annotations, and report errors for the ones that don't make sense

For example @Value.Immutable should not be enabled as-is, since for record X it generates class ImmutableX extends X, which is not valid since records cannot be extended.

elucash commented 3 years ago

Thank you for the interest in Immutable code generation for records.

Question to the maintainers, currently all record-level annotations are skipped, with an Unmatched annotation will be skipped for annotation processing warning.

It's easy to "fix" by adding RECORD case there (with some tricks for backward compatibility).

For implementing record support, do you want to follow a. opt-in approach: explicitly enable annotations that make sense for records or b. opt-out: enable all current class-level annotations, and report errors for the ones that don't make sense

We try reuse most annotations which make sense. So we need to error out ones which are not applicable and allow any other to work. Anyway, most of the annotations will not work "naturally" and we are not very eager to do very deep validation to find every single annotation which is not working for some functionality if put in some wrong place, so there's an expected gap. So it's hard to say a or b, it's, probably, combination of both, I dunno.

At first, I though just extending @Value.Immutable to work on records, but you're right, semantically this doesn't make sense, since record is already immutable, what we're doing is just adding some conveniences like builder/with methods. @Builder.Constructor seems like ok, but emphasize "constructor" (for free form Java-objects) and since record is always and on purpose defined by the primary constructor, it looks like redundant. The @Builder (without nested annotation) on a record can be used to say "just generate builder for it". I would note that for with methods (coming as default impls from generated interface) we will need to specify this yet-to-be-generated interface and in addition I would want to support nested builder which extends generated one, like this

// @Builder
record X(int a, String b) implements WithX {
  // @Builder
  static class Builder extends XBuilder {}
}

We need to place at least one annotation to kick off annotation processor on the record class (or it's nested class). Another option would be to group these generated types under some umbrella type like ImutablesX.{Builder, With}, notice -s ending (it helps with cleanup or incremental compilation, sometimes, to have one generated java class per annotation/annotation processor).

markhobson commented 2 years ago

A further limitation of the @Builder.Constructor workaround for records is that the generated builder cannot be customised. For example, we cannot specify a builder base class like we can for immutables (similar to #1310), and we cannot extend the generated builder ourselves (#1080).

elucash commented 2 years ago

agree

bmarwell commented 11 months ago

Would love to see this in a release!