We independently built a "record companion" generator — it overlaps a lot with record-builder. Sharing the approach (and where they differ) #277
ivan-velikanov
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
First: thanks for record-builder. We only found it after building our own thing (DataHelper), and it was a pleasant surprise to see two projects converge on the same core idea from different directions. This isn't a "mine's better" post — the overlap is real and the differences are mostly about what problem each started from. Sharing in case the angle is useful to anyone here.
https://github.com/xyz-jphil/datahelper
The shared idea
Both are pure compile-time annotation processors (no runtime reflection, no agent) that give you immutable, value-semantic carriers with proper
equals/hashCode. record-builder starts from arecordand adds construction ergonomics. We started from a DTO and added a record projection plus a reflection-free metadata API. Same destination on the immutability axis; different roads.Construction: builder & withers vs. a mutable round-trip
record-builder's builder + wither:
Our generator emits, per type, a readable interface, a writable interface, and an immutable record
Person_R. The mutable fluent POJO is the builder, and a "wither" is just a round-trip:Honestly, for withers the round-trip is more general — one expression changes any number of fields with arbitrary logic in between, no per-field
withXgenerated. The one thing record-builder does that this can't: a staged builder that makesbuild()un-callable until every required field is set (compile-time). The round-trip will happily give you a record with nulls. If you rely on staged builders, record-builder wins that point outright.The part that's genuinely different: reflection-free access by name
This is the bit we built that record-builder doesn't aim at, and it's hard to appreciate until you see what it lets you write. Alongside the record, every type gets:
Person.$name(aField<Person,String>), and aFIELDSlist;getPropertyByName/setPropertyByName(with type coercion) /getPropertyType, all backed by a generatedswitch.No reflection means these generic helpers — written once — work for every DTO and run where reflection doesn't: TeaVM (Java→JS in the browser), GraalVM native images.
Example 1 — bind HTTP query/form params to a DTO (one helper, every DTO, even in the browser)
To do this over a plain
record, you reflect overgetRecordComponents()andinvoke()each accessor — which is slow, untyped, needs GraalVM reflection config, and simply doesn't exist on TeaVM.Example 2 — field-level diff for PATCH / audit logs (works on the immutable record)
Example 3 — "serialize to any sink" without a serialization library
That one map-shaped view feeds a NoSQL document store, a key-value cache, a template engine, a signed-payload builder — anywhere, with no Jackson and no reflection. (We use the same mechanism to read/write a graph DB directly; JSON is just another sink.)
The common thread: type erasure + reflection is what normally forces you to either hand-write per-DTO boilerplate or accept a reflection cost that's unavailable on JS/native targets. Generated symbols + by-name
switchaccess removes both.What record-builder does that we don't
To keep this honest:
Optional/ null-handling options on construction.Why post this here
Mostly because the convergence was striking and the reflection-free-by-name angle might spark ideas — possibly even for record-builder (a generated component-name
switchwould give records the same browser/native-safe dynamic access). Genuinely curious to know your views. Either way, nice work. Record-builder inspired me that a library such as mine could also have a genuine use case, otherwise I believed it is very internal pet project kind of tool.All reactions