# hubspot-immutables **Repository Path**: mirrors_HubSpot/hubspot-immutables ## Basic Information - **Project Name**: hubspot-immutables - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-09-24 - **Last Updated**: 2026-03-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # hubspot-immutables Using [Java Immutables](http://immutables.github.io/) at HubSpot. ### Getting Started Include the following in your `pom.xml` ```xml com.fasterxml.jackson.core jackson-annotations com.hubspot.immutables hubspot-style com.hubspot.immutables immutables-exceptions org.immutables value provided ``` if `com.google.code.findbugs:annotations` is included as a transitive dependency, it will be used during code generation; this means it will become a full dependency in your project. If you wish to avoid it, then you'll need to exclude it from any dependencies that pull it in, otherwise you'll need to explicitly add it. Start writing POJOs as abstract classes: ```java import org.immutables.value.Value.Immutable; @Immutable @HubSpotStyle public abstract class AbstractWidget { public abstract int getPortalId(); public abstract List getFoo(); public abstract Optional getBar(); } ``` or interfaces: ```java @Immutable @HubSpotStyle public interface WidgetIF { int getPortalId(); List getFoo(); Optional getBar(); } ``` both interfaces and Abstracts can have the name `AbstractAbc` or `AbcIF` to create classes named `Abc`: ```java @Immutable @HubSpotStyle public interface AbstractWidget { int getPortalId(); List getFoo(); Optional getBar(); } ``` This will generate a class at compile time called `Widget`, which you can use as follows: ```java Widget widget = Widget.builder() .setPortalId(53) .addFoo(1, 1, 2, 3) .setBar(Optional.of("Hello, World")) .build(); widget.getPortalId(); // 53 widget.getFoo(); // [1, 1, 2, 3] widget.getBar(); // Optional.of("Hello, World") ``` If your project isn't in a compilable state, but still want to have access to the generated classes, compile only the immutable interface in question (This is easily done in intellij by just opening the editor to the file and then Build->"Compile blahblah.java" in the menu. You can add derived properties via default methods: ```java @Immutable @HubSpotStyle public abstract class AbstractWidget { public abstract int getPortalId(); public abstract Set getFoo(); public abstract Optional getBar(); @Value.Derived public boolean portalIdInFoo() { return getFoo().contains(getPortalId()); } } ``` ### A few things to note: 1. Be sure to apply the `@HubSpotStyle`, which will do a few things in the background including 1. Strip the `Abstract` prefix when naming the generated class 2. Ensure that the getter methods are properly recognized 3. Overall, we use it to make `Immutables` across HubSpot a more uniform experience 2. *All* non `@Nullable` fields must be set before calling `build()`, otherwise a `InvalidImmutableStateException` will be thrown. The only exception to this rule are `Optional`s which are detected and defaulted to `empty()`. [See docs](http://immutables.github.io/immutable.html#optional-attributes). 3. Unless explicitly specified (very much discouraged), `null`s are not allowed as field values. [See docs](http://immutables.github.io/immutable.html#nullable-attributes) 4. `ImmutableConditions` methods rely on String.format for formatting, so if there are missing parameters, you may see interesting errors. ### Validation You can also add validation to your built object by annotating a method (or multiple methods) with `@Value.Check`. Validation methods should throw `InvalidImmutableStateException` on invalid input. Message bodies parsed with Jackson server-side will automatically throw a 400 Bad Request on upon encountering such an exception. Elsewhere, the exception will be thrown from the `.build()` call (or any `.with` calls). To assist with validation, the `ImmutableConditions` class is available; it is more or less a drop-in replacement for `Preconditions`/`PublicPreconditions`: ```java @Immutable @HubSpotStyle public interface WidgetIF { int getWidgetId(); Set getFoo(); Optional getBar(); @Value.Check default void widgetIdInFoo() { ImmutableConditions.checkValid(getFoo().contains(getWidgetId()), "widgetId %s must be in Foo!", getWidgetId()); } } ``` Methods annotated with `@Value.Check` will be executed on `.build()`, so once you have a built immutable object, you are guaranteed that it is in a valid state. ### Jackson support Jackson support is always enabled, since we use it extensively. If you wish to deserialize your `AbstractWidget` or `WidgetIF` as a `Widget` you will need to add `@JsonDeserialize(as=Widget.class)` to the annotations of your `WidgetIF` or `AbstractWidget`. However if you are only using your `AbstractWidget` or `WidgetIF` for code generation, and always expecting to receive a `Widget`, you don't need to add any specific Json annotations. `Rosetta` annotations are also passed through, so you can annotate your methods with the correct `Rosetta` annotations, so that those properties are only ever used when working with the DB. *NB: At first adding the annotations will cause your IDE to report an error, as the generated class doesn't exist yet. All will be fine after the project is compiled. This has to do with the way annotation processing works.* ```java @Immutable @HubSpotStyle @JsonDeserialize(as = Widget.class) // Only add this line if you expect to deserialize something to an AbstractWidget public abstract class AbstractWidget { public abstract int getPortalId(); public abstract Optional getUserEmail(); public abstract List getWidgetStrings(); @RosettaProperty("hubspot_id") public abstract int getHubSpotId(); } ``` ### Egg Pattern Sometimes you're going to want to accept an object in a `POST` that doesn't contain an ID. Rather than making your ID `Optional`, or `@Nullable`, when you know that they are almost never missing, and you don't want to check for them. That is when the Egg pattern is a good idea: ```java public interface FooCore { String getName(); Optional getDescription(); } @Immutable @HubSpotStyle public interface FooEggIF extends FooCore { } @Immutable @HubSpotStyle public interface FooIF extends FooCore { int getId(); } ``` With this pattern your `POST` methods can accept a `FooEgg`, and you can do something like this: ```java @POST public Foo createFoo(FooEgg toCreate) { int fooId = fooDao.create(toCreate); return Foo.builder() .from(toCreate) .setId(fooId) .build(); } ``` ### Modifiable Pattern This is similar to the Egg Pattern above, however you might want to use it if there are some fields that are required on your object, but you might not always receive them from the client. The Modifiable object does not perform any validation until `toImmutable` is called. ```java @HubSpotStyle @Value.Immutable @Value.Modifiable public interface BarIF { String getName(); String getDescription(); } @PUT @Path("bars/{name}/description") public Bar updateDescription(@PathParam("name") String barName, ModifiableBar modifiableBar) { Bar bar = modifiableBar .setName(barName) .toImmutable(); barDao.update(bar); return bar; } ``` ### Normalization Since immutables 2.1.14 we've been able to use normalization with our classes. This means that we can perform things like `String.trim()` during build time. It is done by a special `@Value.Check` that returns the interface, and is always named `normalize`. The key things to remember are 1. Always do a test first to see if the instance is already normalized 2. Validation has to happen in your `normalize` method 3. Perform your validations after the normalization, so it only gets performed on your normalized instances 4. There can **ONLY BE ONE** `@Value.Check` method An example is below: ```java @HubSpotStyle @Value.Immutable public interface NormalizedWidgetIF { Optional getOptionalBigDecimal(); String getCleanEmail(); @Value.Check default NormalizedWidgetIF normalize() { if (!isNormalized()) { return NormalizedWidget.builder() .from(this) .setOptionalBigDecimal(getOptionalBigDecimal().map(bigDecimal -> bigDecimal.setScale(6, BigDecimal.ROUND_HALF_UP))) .setCleanEmail(getCleanEmail().trim()) .build(); } ImmutableConditions.checkValid(getOptionalBigDecimal().isPresent() && getOptionalBigDecimal().get().compareTo(BigDecimal.ZERO) > 0, "Optional BigDecimal must be greater than zero"); return this; } @JsonIgnore @Value.Auxiliary default boolean isNormalized() { return getOptionalBigDecimal().map(bigDecimal -> bigDecimal.scale() == 6).orElse(true) && getCleanEmail().equals(getCleanEmail().trim()); } } ``` ### Setup Intellij for `Immutables` *Based on https://immutables.github.io/apt.html#intellij-idea* To work with `Immutables` you must enable annotation processing. To do so globally follow these steps: 1. Go to *File → New Project Setup → Preferences for New Projects...* (This varies with IntelliJ versions) 2. Under *Compiler → Annotation Processors* toggle `Enable annotation processing` 3. Set `Store generated sources relative to:` to `Module content root` 4. Set the sources directories to `target/generated-sources/annotations` and `target/generated-test-sources/test-annotations` respectively. 5. Under *Build Tools → Maven → Importing* select `Generated sources folders: Detect automatically`. You may have to restart IntelliJ (File → Invalidate Caches and Restart...) for it to pick up all the changes successfully. You should now be able to use your generated classes, and build, test and run your code inside Intellij as usual. IntelliJ compiler seeing the generated classes, but not showing up for autocomplete/autoimport? Try a maven project refresh. ### Help! If you're finding that your immutable classes are not being generated; it's possible that there is an unreported error in your code; in that case add this compiler configuration: ```xml 1000000 ``` so it looks like something below: ```xml org.apache.maven.plugins maven-compiler-plugin 1000000 ``` Then run `mvn clean compile -X` and you'll get the errors out!