Jakarta JAXB (GlassFish implementation) does NOT fully support Java records for XML marshalling/unmarshalling.
Error Encountered:
org.glassfish.jaxb.runtime.v2.runtime.IllegalAnnotationsException: 272 counts of IllegalAnnotationExceptions
JAXB annotation is placed on a method that is not a JAXB property
AtomEntryDto does not have a no-arg default constructor
Java Records (Java 14+):
- Immutable data carriers
- All fields are implicitly
final - Compact canonical constructor only
- Accessor methods (not JavaBean getters):
field()instead ofgetField() - Cannot have additional constructors beyond canonical
- Class itself is implicitly
final
JAXB Requirements (JavaBeans pattern):
- Mutable objects with default no-arg constructor
- Non-final fields with setters
- JavaBean-style getters:
getField() - Ability to instantiate empty object and populate via setters
-
No Default Constructor:
public record AtomEntryDto(String id, String title, ...) {} // JAXB Error: "does not have a no-arg default constructor"
- Records only have canonical constructor
- Cannot add no-arg constructor
- Lombok's
@NoArgsConstructordoes NOT work on records
-
Method Annotations Not Recognized:
@XmlElement(name = "id", namespace = "...") String id // Record generates: public String id() { return id; } // JAXB expects: public String getId() { return id; } // Error: "JAXB annotation is placed on a method that is not a JAXB property"
-
Immutability:
- Records have no setters
- JAXB unmarshalling requires setters to populate fields
- Cannot use
@XmlAccessType.FIELDeffectively with final fields
@NoArgsConstructor // Does NOT compile with records
public record AtomEntryDto(...) {}Error: Lombok constructor annotations are not supported on records
public record AtomEntryDto(...) {
public AtomEntryDto() { // COMPILE ERROR
this(null, null, ...);
}
}Error: Records cannot have additional constructors beyond canonical/compact
@XmlAccessorType(XmlAccessType.FIELD)
public record AtomEntryDto(...) {}Still Fails: JAXB tries to access fields via reflection but records protect field access
Effort: ~12-15 hours for 40+ DTOs
Implementation:
// FROM (Record):
public record CustomerDto(
String uuid,
OrganisationDto organisation,
CustomerKind kind
) {}
// TO (Class):
@XmlRootElement(name = "Customer", namespace = "http://naesb.org/espi/customer")
@XmlAccessorType(XmlAccessType.FIELD)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CustomerDto {
@XmlTransient
private String uuid;
@XmlElement(name = "Organisation", namespace = "http://naesb.org/espi/customer")
private OrganisationDto organisation;
@XmlElement(name = "kind", namespace = "http://naesb.org/espi/customer")
private CustomerKind kind;
}Benefits:
- ✅ Full JAXB compatibility
- ✅ Proper namespace prefix support (
cust:,espi:) - ✅ Lombok reduces boilerplate
- ✅ Standard JavaBeans pattern
Drawbacks:
- ❌ Lose immutability
- ❌ More verbose (getters/setters visible in code)
- ❌ Larger codebase
Effort: ~2-4 hours
Implementation: Replace GlassFish JAXB runtime with MOXy:
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>org.eclipse.persistence.moxy</artifactId>
<version>4.0.2</version>
</dependency>Status: MOXy has EXPERIMENTAL record support but still has limitations with immutability.
Risks:
⚠️ MOXy record support incomplete⚠️ Different JAXB implementation may have other issues⚠️ Less commonly used than GlassFish JAXB
Why Not Recommended:
- Jackson ignores
@XmlNsprefix declarations in package-info.java - Auto-generates prefixes (
wstxns1,wstxns2) instead ofespi:,cust: - No configuration option to override this behavior
- Fails ESPI 4.0 compliance for namespace prefixes
Convert all DTOs from records to Lombok-annotated classes (Option A).
Reasoning:
- Full Jakarta JAXB compliance
- Proper namespace prefix support per ESPI 4.0 specification
- Lombok mitigates boilerplate concerns
- Standard JavaBeans pattern widely understood
- MapStruct already works with Lombok classes (no changes needed)
- Entities are already Lombok classes (consistent architecture)
Atom Domain (2):
- AtomEntryDto
- AtomFeedDto
- LinkDto
Usage Domain (~20):
- UsagePointDto
- MeterReadingDto
- IntervalBlockDto
- IntervalReadingDto
- ReadingTypeDto
- DateTimeIntervalDto
- ReadingQualityDto
- TimeConfigurationDto
- UsageSummaryDto
- ElectricPowerQualitySummaryDto
- ApplicationInformationDto
- AuthorizationDto
- SubscriptionDto
- BatchListDto
- LineItemDto
- SummaryMeasurementDto
- BillingChargeSourceDto
- TariffRiderRefDto
- TariffRiderRefsDto
- (+ others)
Customer Domain (~10):
- CustomerDto (already has namespace annotations)
- CustomerAccountDto
- CustomerAgreementDto
- ServiceLocationDto
- StatementDto
- MeterDto
- EndDeviceDto
- (+ nested records in CustomerDto)
Shared DTOs (~5):
- RationalNumberDto
- ReadingInterharmonicDto
- Various embedded records
- Create Lombok class templates for common patterns
- Convert DTOs domain-by-domain (atom → usage → customer)
- Update MapStruct mappers (should work without changes)
- Run tests after each domain (verify XML output)
- Validate namespace prefixes in generated XML
- Template creation: 1 hour
- Atom domain conversion: 1 hour
- Usage domain conversion: 6 hours
- Customer domain conversion: 3 hours
- Testing and validation: 2 hours
- TOTAL: ~13 hours
- Java Records Specification (JEP 395)
- Jakarta XML Binding Specification
- Lombok Documentation - Constructor Annotations
- JAXB with Records Discussion (Stack Overflow)
Document Created: 2026-01-19 Status: BLOCKING - Requires architectural decision before proceeding