Custom JSON marshalling in Grails for flexible API responses
For Australian development teams shipping APIs from Sydney, Melbourne, or Brisbane offices, the way a Grails application serialises objects can shape how easily downstream services integrate with their systems. Out of the box, the framework relies on Groovy's object graphs and the JSON converter, but those defaults often leave sensitive fields exposed, embed Hibernate proxies, or omit the pagination metadata expected by Australian fintech clients working with APRA-aligned reporting endpoints. Knowing when to step past the defaults is what separates a prototype from a production-grade service.
Grails has shipped JSON Views since version 3 and improved marshalling APIs in successive releases. The latest releases add finer control over how date, currency, and enum values flow through the converter, particularly relevant when systems must serialise amounts in Australian dollars with the right precision or render timestamps in AEST. A custom marshaller gives developers a single, predictable place to format a domain class, enforce field visibility, and skip proxies left over from lazy associations.
Many teams working on data-sharing integrations with the Australian Bureau of Statistics, ASX market feeds, or state-based health portals discover early that the default JSON output does not match the schema mandated by those publishers. Marshalling rules baked into the framework tend to dump every property, including transient ones, and that can quickly lead to leaking internal identifiers that should never leave the trust boundary. Customising the output is therefore not a luxury but a baseline requirement for compliant API design.
The remainder of this guide walks through the practical steps for adding custom marshallers to a Grails application, from defining a marshaller class to registering it through a Spring bean, plus testing patterns that help Sydney-based teams catch regressions before their CI pipeline promotes a build to staging. The techniques below apply to both monolithic deployments on AWS Sydney and distributed services running across multiple regions.
Built-in views and the JSON converter baseline
Grails offers two primary pathways for shaping JSON output: the GSP-based JSON Views introduced in version 3, and the older JSON converter that goes back to the Groovy 1.8 era. JSON Views use Groovy Server Pages templates to render responses declaratively, while the converter walks an object graph using reflection. For control over deeply nested structures, the converter path remains popular, especially when developers need to reuse marshalling logic across REST endpoints and message-driven services.
The default converter behaviour walks every readable property and serialises it using Groovy's standard rules. Dates become ISO strings, BigDecimal values keep their scale, and enums use their name() value. While convenient, this behaviour can backfire when a domain class contains fields marked for internal auditing, when a Hibernate proxy sneaks in, or when a lazy: false collection contains thousands of records that should never appear in a list response.
For teams serving Australian clients who expect amounts rendered to two decimal places and dates in a local format, the converter's defaults are a starting point rather than an endpoint. Customising the output means deciding whether to override rendering at the field level, at the class level, or by registering a dedicated marshaller bean that intercepts the conversion process for specific classes. The choice depends on how broadly the customisation applies across the application surface.
When projects adopt both JSON Views and custom marshallers, the JSON View usually wins because it offers stronger schema discipline. Marshallers remain the right tool for global rules, such as always masking certain identifiers or always formatting currency fields, regardless of which view consumes the object.
Writing a custom marshaller class
A marshaller in Grails is a class extending grails.converters.JSON and registering itself with the converter. The traditional approach involves extending AbstractMarshaller or implementing ObjectMarshaller, depending on the version. Developers typically create a class in grails-app/utils/ or under src/main/groovy/ that handles one or more target types.
The skeleton looks roughly like this in practice:
import grails.converters.JSON
import org.grails.web.converters.marshaller.ObjectMarshaller
class AccountMarshaller implements ObjectMarshaller<JSON> {
boolean supports(Object object) {
return object instanceof Account
}
void marshalObject(Object object, JSON converter) throws ConverterException {
Account account = (Account) object
converter.startObject()
converter.property('id', account.id)
converter.property('displayName', account.displayName)
converter.property('balance', account.balance.setScale(2, BigDecimal.ROUND_HALF_UP))
converter.property('currency', 'AUD')
converter.property('createdAt', account.dateCreated.format('dd/MM/yyyy', TimeZone.getTimeZone('Australia/Sydney')))
converter.endObject()
}
}
The marshalObject method gives complete control over the field order, formatting, and inclusion logic. Returning currency formatted to two decimal places, tagging the response with currency: AUD, and rendering creation timestamps in Australian format are common requirements for APIs serving local banks or NDIS providers.
Developers working on teams that have to align with the Notifiable Data Breaches scheme under the Office of the Australian Information Commissioner will appreciate the explicit approach: nothing leaves the marshaller unless the developer wrote it into the startObject block. That makes auditing and PII review a much smaller task compared to diffing reflection-based output.
Marshallers can also encode composite types, such as embedding a sanitised merchant descriptor or trimming a tax file number to its last three digits before exposure. Putting that logic in a marshaller rather than in every controller keeps the sanitisation rules in one place and resilient against future refactors.
Registering marshallers through the Spring context
Once the marshaller class exists, it must be registered so the JSON converter knows when to call it. The cleanest way is to declare it as a Spring bean inside grails-app/conf/spring/resources.groovy or by annotating the class with @Component. Spring picks up annotated beans during context startup, and the converter consults the registry during serialisation.
A typical registration snippet reads accountMarshaller(AccountMarshaller) { } inside the beans block of resources.groovy. When the marshaller is registered as a bean, Grails wires it into the JsonMarshallerRegistry automatically. Teams that prefer Java configuration can use a @Configuration class with a @Bean method returning the marshaller instance.
For applications that need to disable certain built-in marshallers, such as the default date or enum converters, the registry exposes a removal API. This becomes handy when an Australian retailer wants all dates to appear in AEST and at midnight precision, overriding the UTC output that the default marshaller produces. Removing the old behaviour and supplying a tailored replacement prevents accidental serialisation in the wrong timezone.
Care should be taken when registering marshallers for interfaces or abstract classes. The supports method receives the runtime object, so a marshaller targeting Payment will match every subclass, including BpayPayment, EftposPayment, and CardPayment if those are subtypes. That is usually desirable in payment systems handling BPAY or eftpos flows, but it is worth confirming before relying on the inheritance match.
Handling collections, pagination, and nested graphs
APIs that return collections need more than per-object marshallers. List responses should include metadata such as total counts, page size, and current offset. The Grails PagedResultList and similar helpers can be wrapped in a custom marshaller that emits a stable shape, which downstream clients across Australia can rely on regardless of how the underlying query evolves.
A common pattern is to register a marshaller for grails.gorm.PagedResultList that emits the list, total count, and offset alongside each result. Each element is then passed through the per-class marshaller, keeping the response consistent across endpoints.
Nested object graphs deserve special attention. Lazy Hibernate associations can throw LazyInitializationException if the marshaller runs outside a transaction. Developers in Sydney and Melbourne typically solve this by either eagerly fetching the associations through a HQL fetch join, by using Hibernate's initialize() inside the marshaller, or by attaching an OpenSessionInView filter where appropriate. Each approach has trade-offs around performance and transaction scope.
For deeply nested responses, such as an account hierarchy with linked transactions and merchant descriptors, marshalling performance becomes measurable. Caching the marshalled output for immutable subtrees, or short-circuiting cycles with a visited-object set, keeps response times stable when API consumers hit heavy endpoints. Memory profiling under load reveals whether the caching pays off.
Australian teams serving the ASX market data pipeline frequently serialise large lists of instrument metadata. A marshaller that skips null fields and uses compact output helps reduce bandwidth, which matters when responses travel across the NBN or a corporate VPN with constrained throughput.
Testing and securing marshalled output
Custom marshallers should be tested as part of the regular test suite. Grails offers integration testing through IntegrationSpec (or Specification in newer versions) and unit testing through plain JUnit or Spock. A marshaller unit test typically loads a sample domain instance, runs the converter against it, and asserts on the resulting JSON.
Security deserves focused attention. Australian projects that fall under the Privacy Act and the Australian Privacy Principles must verify that no PII slips through a new marshaller. Reviewing the privacy policy page on the Grails Example site summarises how the framework approaches sensitive fields, and a practical exercise is to dump every JSON endpoint, then run a static check against known sensitive field names such as tfn, medicareNumber, or dateOfBirth.
Marshalling tests should also cover edge cases: null associations, empty collections, dates in AEST around daylight saving boundaries, and currencies with unusual precision. A test that exercises the transition between Australian Eastern Standard Time and Australian Eastern Daylight Time catches the classic off-by-one-hour bug in timestamp formatting.
Finally, hooking the marshaller into an automated contract test gives early warning when a downstream consumer expects a field that the marshaller has stopped emitting. Tools like Pact, combined with a CI job running on a build agent in the ap-southeast-2 region, can detect such drift before it reaches production.
The path to reliable JSON output in Grails runs through clear, hand-written marshalling logic, careful registration, and steady regression coverage. To see complete working snippets and downloadable scaffolding, browse the code examples on the Grails Example site. With those building blocks in place, Australian engineering teams can ship APIs that respect local formatting conventions, comply with privacy expectations, and stay stable as their domain models evolve.