A generated DTO contains the fields from your annotated source class (after ignores, nesting, and validation are applied), plus getters/setters, equals, hashCode, toString, and optionally Lombok builder code. That is the whole contract. Extra fields and custom methods are not generated — you add those yourself, almost always by subclassing the generated type or wrapping it.
You could. But then you own two copies of the same field list: the domain class and the DTO. Add a field to one, forget the other, and the bug waits for you in production. The generated base refreshes on every compile; your subclass just inherits whatever changed. You may need to fix a super(...) call when the constructor gains a parameter — but you are not transcribing the whole field list every time the domain moves.
Most teams keep DTOs thin — data plus accessors — with real logic in services or the domain layer. When you need a bit more on the type (an extra computed field, a helper method, a trace ID), a subclass is where that lives.
The generated class is a public class with a public all-args constructor and the usual Object methods. The exact shape is in DtoClassWriter / DtoGenerator in this repo if you want the details.
Source model:
@GenerateDto
public class User {
private String name;
private int age;
// accessors…
}Generated: something like UserDto with name, age, constructor, accessors, equals, hashCode, toString.
Your extension:
import java.util.Objects;
public final class UserDtoWithTrace extends UserDto {
private String requestId;
public UserDtoWithTrace(String name, int age, String requestId) {
super(name, age);
this.requestId = requestId;
}
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
}After mvn compile, UserDto is regenerated; UserDtoWithTrace is yours and stays put. When the domain changes, UserDto reflects it first; your subclass picks up the new base API and you fix super(...) (and sometimes equals / hashCode) as needed — see the next sections.
Generated equals / hashCode only consider generated fields.
equalsusesgetClass(), so a subclass instance is not equal to a baseUserDtowith the same values.- Subclass-only fields are ignored by the inherited methods. Two
UserDtoWithTraceobjects with the samename/agebut differentrequestIdwould still test equal if you don't override — usually a bug.
Fix: override both on the subclass and include every field that matters (parent fields via getters + your own fields). Don't assume super.equals(o) is enough: the parent logic still ignores requestId.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserDtoWithTrace that = (UserDtoWithTrace) o;
return Objects.equals(getName(), that.getName())
&& getAge() == that.getAge()
&& Objects.equals(requestId, that.requestId);
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge(), requestId);
}Same fields in equals and hashCode, as usual.
UserDto vs UserDtoWithTrace: the generator does not treat them as interchangeable value types. Compare fields manually in app code, or prefer a wrapper holding a plain UserDto — see DTOs and interfaces (wrapper pattern).
Methods only: if you add methods that don't introduce new state, the inherited equals / hashCode are unchanged. New fields mean you override.
Generated toString only prints generated fields. Override if you want logs to show subclass state.
Subclassing gets awkward with @SuperBuilder hierarchies. Prefer composition or a hand-written outer type when builders are central — details in DTO builder pattern.
| Topic | What to do |
|---|---|
| Extra fields / methods | Subclass (or wrapper) |
| What updates automatically | The generated base picks up domain changes (fields, types, mapping, validation, builder defaults, …); your subclass inherits that |
| Regeneration | Base class only; tweak subclass (super(...), sometimes equals / hashCode) when the base API changes |
equals / hashCode |
Override when the subclass has extra state |
| Base vs subclass "same data" | Not built-in; compare explicitly or compose |
Some codebases keep DTOs as pure POJOs; others add small instance or static methods — formatting, isEmpty()-style checks, tiny factories. That is a style choice. Keep DTOs shallow and avoid real domain logic there, but the generator does not stop you: put those methods on the same subclass (or wrapper) as extra fields. Regeneration never touches them.
If a method only uses inherited getters, equality behavior stays as above. If you add new fields the method depends on for identity, override equals / hashCode like any other subclass with extra state.