import grails.rest.*
@Resource(uri='/books')
class Book {
String title
static constraints = {
title blank:false
}
}
10 REST
Version: 8.0.0
Table of Contents
10 REST
REST is not really a technology in itself, but more an architectural pattern. REST is very simple and just involves using plain XML or JSON as a communication medium, combined with URL patterns that are "representational" of the underlying system, and HTTP methods such as GET, PUT, POST and DELETE.
Each HTTP method maps to an action type. For example GET for retrieving data, POST for creating data, PUT for updating and so on.
Grails includes flexible features that make it easy to create RESTful APIs. Creating a RESTful resource can be as simple as one line of code, as demonstrated in the next section.
10.1 Domain classes as REST resources
The easiest way to create a RESTful API in Grails is to expose a domain class as a REST resource. This can be done by adding the grails.rest.Resource transformation to any domain class:
Simply by adding the Resource transformation and specifying a URI, your domain class will automatically be available as a REST resource in either XML or JSON formats. The transformation will automatically register the necessary RESTful URL mapping and create a controller called BookController.
You can try it out by adding some test data to BootStrap.groovy:
def init = {
new Book(title:"The Stand").save()
new Book(title:"The Shining").save()
}
And then hitting the URL http://localhost:8080/books/1, which will render the response like:
<?xml version="1.0" encoding="UTF-8"?>
<book id="1">
<title>The Stand</title>
</book>
If you change the URL to http://localhost:8080/books/1.json you will get a JSON response such as:
{"id":1,"title":"The Stand"}
If you wish to change the default to return JSON instead of XML, you can do this by setting the formats attribute of the Resource transformation:
import grails.rest.*
@Resource(uri='/books', formats=['json', 'xml'])
class Book {
...
}
With the above example JSON will be prioritized. The list that is passed should contain the names of the formats that the resource should expose. The names of formats are defined in the grails.mime.types setting of application.groovy:
grails.mime.types = [
...
json: ['application/json', 'text/json'],
...
xml: ['text/xml', 'application/xml']
]
Or the equivalent in application.yml:
grails:
mime:
types:
json:
- 'application/json'
- 'text/json'
xml:
- 'text/xml'
- 'application/xml'
See the section on Configuring Mime Types in the user guide for more information.
Instead of using the file extension in the URI, you can also obtain a JSON response using the ACCEPT header. Here’s an example using the Unix curl tool:
$ curl -i -H "Accept: application/json" localhost:8080/books/1
{"id":1,"title":"The Stand"}
This works thanks to Grails' Content Negotiation features.
You can create a new resource by issuing a POST request:
$ curl -i -X POST -H "Content-Type: application/json" -d '{"title":"Along Came A Spider"}' localhost:8080/books
HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
...
Updating can be done with a PUT request:
$ curl -i -X PUT -H "Content-Type: application/json" -d '{"title":"Along Came A Spider"}' localhost:8080/books/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
...
Finally a resource can be deleted with DELETE request:
$ curl -i -X DELETE localhost:8080/books/1
HTTP/1.1 204 No Content
Server: Apache-Coyote/1.1
...
As you can see, the Resource transformation enables all of the HTTP method verbs on the resource. You can enable only read-only capabilities by setting the readOnly attribute to true:
import grails.rest.*
@Resource(uri='/books', readOnly=true)
class Book {
...
}
In this case POST, PUT and DELETE requests will be forbidden.
10.2 Mapping to REST resources
If you prefer to keep the declaration of the URL mapping in your UrlMappings.groovy file then simply removing the uri attribute of the Resource transformation and adding the following line to UrlMappings.groovy will suffice:
"/books"(resources:"book")
Extending your API to include more end points then becomes trivial:
"/books"(resources:"book") {
"/publisher"(controller:"publisher", method:"GET")
}
The above example will expose the URI /books/1/publisher.
A more detailed explanation on creating RESTful URL mappings can be found in the URL Mappings section of the user guide.
10.3 Linking to REST resources from GSP pages
The link tag offers an easy way to link to any domain class resource:
<g:link resource="${book}">My Link</g:link>
However, currently you cannot use g:link to link to the DELETE action and most browsers do not support sending the DELETE method directly.
The best way to accomplish this is to use a form submit:
<form action="/book/2" method="post">
<input type="hidden" name="_method" value="DELETE"/>
</form>
Grails supports overriding the request method via the hidden _method parameter. This is for browser compatibility purposes. This is useful when using restful resource mappings to create powerful web interfaces.
To make a link fire this type of event, perhaps capture all click events for links with a data-method attribute and issue a form submit via JavaScript.
The same override can be requested with an X-HTTP-Method-Override header only while the hidden HTTP method
filter is enabled; it is disabled by default as of Grails 8.
By default the _method parameter is resolved inside the dispatcher, after multipart handling and after the
servlet filter chain. <g:form> emits the parameter as it always has. A resources mapping additionally routes
a POST to the member URL at the update action, so a client that cannot send the parameter — an AngularJS
$resource and the clients modelled on it POST to save an existing object — reaches update without it.
The previous behaviour — a servlet filter rewriting the request method ahead of the dispatcher — can be restored:
grails:
web:
hiddenmethod:
filter:
enabled: true
With the filter disabled, servlet filters and the Spring Security filter chain see the request’s real
POST method, and the URL does not distinguish update from delete. Authorization rules keyed on the HTTP
method must be reviewed.
|
10.4 Versioning REST resources
A common requirement with a REST API is to expose different versions at the same time. There are a few ways this can be achieved in Grails.
Versioning using the URI
A common approach is to use the URI to version APIs (although this approach is discouraged in favour of Hypermedia). For example, you can define the following URL mappings:
"/books/v1"(resources:"book", namespace:'v1')
"/books/v2"(resources:"book", namespace:'v2')
That will match the following controllers:
package myapp.v1
class BookController {
static namespace = 'v1'
}
package myapp.v2
class BookController {
static namespace = 'v2'
}
This approach has the disadvantage of requiring two different URI namespaces for your API.
Versioning with the Accept-Version header
As an alternative Grails supports the passing of an Accept-Version header from clients. For example you can define the following URL mappings:
"/books"(version:'1.0', resources:"book", namespace:'v1')
"/books"(version:'2.0', resources:"book", namespace:'v2')
Then in the client simply pass which version you need using the Accept-Version header:
$ curl -i -H "Accept-Version: 1.0" -X GET http://localhost:8080/books
Versioning using Hypermedia / Mime Types
Another approach to versioning is to use Mime Type definitions to declare the version of your custom media types (see the section on "Hypermedia as the Engine of Application State" for more information about Hypermedia concepts). For example, in application.groovy you can declare a custom Mime Type for your resource that includes a version parameter (the 'v' parameter):
grails.mime.types = [
all: '*/*',
book: "application/vnd.books.org.book+json;v=1.0",
bookv2: "application/vnd.books.org.book+json;v=2.0",
...
}
Or the equivalent in application.yml:
grails:
mime:
types:
all: '*/*'
book: "application/vnd.books.org.book+json;v=1.0"
bookv2: "application/vnd.books.org.book+json;v=2.0"
...
| It is critical that place your new mime types after the 'all' Mime Type because if the Content Type of the request cannot be established then the first entry in the map is used for the response. If you have your new Mime Type at the top then Grails will always try and send back your new Mime Type if the requested Mime Type cannot be established. |
Then override the renderer (see the section on "Customizing Response Rendering" for more information on custom renderers) to send back the custom Mime Type in grails-app/conf/spring/resourses.groovy:
import grails.rest.render.json.*
import grails.web.mime.*
beans = {
bookRendererV1(JsonRenderer, myapp.v1.Book, new MimeType("application/vnd.books.org.book+json", [v:"1.0"]))
bookRendererV2(JsonRenderer, myapp.v2.Book, new MimeType("application/vnd.books.org.book+json", [v:"2.0"]))
}
Then update the list of acceptable response formats in your controller:
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml', 'book', 'bookv2']
// ...
}
Then using the Accept header you can specify which version you need using the Mime Type:
$ curl -i -H "Accept: application/vnd.books.org.book+json;v=1.0" -X GET http://localhost:8080/books
10.5 Implementing REST controllers
The Resource transformation is a quick way to get started, but typically you’ll want to customize the controller logic, the rendering of the response or extend the API to include additional actions.
10.5.1 Extending the RestfulController super class
The easiest way to get started doing so is to create a new controller for your resource that extends the grails.rest.RestfulController super class. For example:
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml']
BookController() {
super(Book)
}
}
To customize any logic you can just override the appropriate action. The following table provides the names of the action names and the URIs they map to:
| HTTP Method | URI | Controller Action |
|---|---|---|
GET |
/books |
index |
GET |
/books/create |
create |
POST |
/books |
save |
GET |
/books/${id} |
show |
GET |
/books/${id}/edit |
edit |
PUT |
/books/${id} |
update |
PATCH |
/books/${id} |
patch |
DELETE |
/books/${id} |
delete |
The create and edit actions are only needed if the controller exposes an HTML interface.
|
As an example, if you have a nested resource then you would typically want to query both the parent and the child identifiers. For example, given the following URL mapping:
"/authors"(resources:'author') {
"/books"(resources:'book')
}
You could implement the nested controller as follows:
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml']
BookController() {
super(Book)
}
@Override
protected Book queryForResource(Serializable id) {
Book.where {
id == id && author.id == params.authorId
}.find()
}
}
The example above subclasses RestfulController and overrides the protected queryForResource method to customize the query for the resource to take into account the parent resource.
Customizing Data Binding In A RestfulController Subclass
The RestfulController class contains code which does data binding for actions like save and update. The class defines a getObjectToBind() method which returns a value which will be used as the source for data binding. For example, the update action does something like this…
class RestfulController<T> {
def update() {
T instance = // retrieve instance from the database...
instance.properties = getObjectToBind()
// ...
}
// ...
}
By default the getObjectToBind() method returns the request object. When the request object is used as the binding source, if the request has a body then the body will be parsed and its contents will be used to do the data binding, otherwise the request parameters will be used to do the data binding. Subclasses of RestfulController may override the getObjectToBind() method and return anything that is a valid binding source, including a Map or a DataBindingSource. For most use cases binding the request is appropriate but the getObjectToBind() method allows for changing that behavior where desired.
Using custom subclass of RestfulController with Resource annotation
You can also customize the behaviour of the controller that backs the Resource annotation.
The class must provide a constructor that takes a domain class as its argument. The second constructor is required for supporting Resource annotation with readOnly=true.
This is a template that can be used for subclassed RestfulController classes used in Resource annotations:
class SubclassRestfulController<T> extends RestfulController<T> {
SubclassRestfulController(Class<T> domainClass) {
this(domainClass, false)
}
SubclassRestfulController(Class<T> domainClass, boolean readOnly) {
super(domainClass, readOnly)
}
}
You can specify the super class of the controller that backs the Resource annotation with the superClass attribute.
import grails.rest.*
@Resource(uri='/books', superClass=SubclassRestfulController)
class Book {
String title
static constraints = {
title blank:false
}
}
10.5.2 Implementing REST Controllers Step by Step
If you don’t want to take advantage of the features provided by the RestfulController super class, then you can implement each HTTP verb yourself manually. The first step is to create a controller:
$ grails create-controller book
Then add some useful imports and enable readOnly by default:
import grails.gorm.transactions.*
import static org.springframework.http.HttpStatus.*
import static org.springframework.http.HttpMethod.*
@Transactional(readOnly = true)
class BookController {
...
}
Recall that each HTTP verb matches a particular Grails action according to the following conventions:
| HTTP Method | URI | Controller Action |
|---|---|---|
GET |
/books |
index |
GET |
/books/${id} |
show |
GET |
/books/create |
create |
GET |
/books/${id}/edit |
edit |
POST |
/books |
save |
PUT |
/books/${id} |
update |
PATCH |
/books/${id} |
patch |
DELETE |
/books/${id} |
delete |
The create and edit actions are already required if you plan to implement an HTML interface for the REST resource. They are there in order to render appropriate HTML forms to create and edit a resource. They can be discarded if that is not a requirement.
|
The key to implementing REST actions is the respond method introduced in Grails 2.3. The respond method tries to produce the most appropriate response for the requested content type (JSON, XML, HTML etc.)
Implementing the 'index' action
For example, to implement the index action, simply call the respond method passing the list of objects to respond with:
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond Book.list(params), model:[bookCount: Book.count()]
}
Note that in the above example we also use the model argument of the respond method to supply the total count. This is only required if you plan to support pagination via some user interface.
The respond method will, using Content Negotiation, attempt to reply with the most appropriate response given the content type requested by the client (via the ACCEPT header or file extension).
If the content type is established to be HTML then a model will be produced such that the action above would be the equivalent of writing:
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
[bookList: Book.list(params), bookCount: Book.count()]
}
By providing an index.gsp file you can render an appropriate view for the given model. If the content type is something other than HTML then the respond method will attempt to lookup an appropriate grails.rest.render.Renderer instance that is capable of rendering the passed object. This is done by inspecting the grails.rest.render.RendererRegistry.
By default there are already renderers configured for JSON and XML, to find out how to register a custom renderer see the section on "Customizing Response Rendering".
Implementing the 'show' action
The show action, which is used to display and individual resource by id, can be implemented in one line of Groovy code (excluding the method signature):
def show(Book book) {
respond book
}
By specifying the domain instance as a parameter to the action Grails will automatically attempt to lookup the domain instance using the id parameter of the request. If the domain instance doesn’t exist, then null will be passed into the action. The respond method will return a 404 error if null is passed otherwise once again it will attempt to render an appropriate response. If the format is HTML then an appropriate model will produced. The following action is functionally equivalent to the above action:
def show(Book book) {
if(book == null) {
render status:404
}
else {
return [book: book]
}
}
Implementing the 'save' action
The save action creates new resource representations. To start off, simply define an action that accepts a resource as the first argument and mark it as Transactional with the grails.gorm.transactions.Transactional transform:
@Transactional
def save(Book book) {
...
}
Then the first thing to do is check whether the resource has any validation errors and if so respond with the errors:
if(book.hasErrors()) {
respond book.errors, view:'create'
}
else {
...
}
In the case of HTML the 'create' view will be rendered again so the user can correct the invalid input. In the case of other formats (JSON, XML etc.), the errors object itself will be rendered in the appropriate format and a status code of 422 (UNPROCESSABLE_ENTITY) returned.
If there are no errors then the resource can be saved and an appropriate response sent:
book.save flush:true
withFormat {
html {
flash.message = message(code: 'default.created.message', args: [message(code: 'book.label', default: 'Book'), book.id])
redirect book
}
'*' { render status: CREATED }
}
In the case of HTML a redirect is issued to the originating resource and for other formats a status code of 201 (CREATED) is returned.
Implementing the 'update' action
The update action updates an existing resource representation and is largely similar to the save action. First define the method signature:
@Transactional
def update(Book book) {
...
}
If the resource exists then Grails will load the resource, otherwise null is passed. In the case of null, you should return a 404:
if(book == null) {
render status: NOT_FOUND
}
else {
...
}
Then once again check for errors validation errors and if so respond with the errors:
if(book.hasErrors()) {
respond book.errors, view:'edit'
}
else {
...
}
In the case of HTML the 'edit' view will be rendered again so the user can correct the invalid input. In the case of other formats (JSON, XML etc.) the errors object itself will be rendered in the appropriate format and a status code of 422 (UNPROCESSABLE_ENTITY) returned.
If there are no errors then the resource can be saved and an appropriate response sent:
book.save flush:true
withFormat {
html {
flash.message = message(code: 'default.updated.message', args: [message(code: 'book.label', default: 'Book'), book.id])
redirect book
}
'*' { render status: OK }
}
In the case of HTML a redirect is issued to the originating resource and for other formats a status code of 200 (OK) is returned.
Implementing the 'delete' action
The delete action deletes an existing resource. The implementation is largely similar to the update action, except the delete() method is called instead:
book.delete flush:true
withFormat {
html {
flash.message = message(code: 'default.deleted.message', args: [message(code: 'Book.label', default: 'Book'), book.id])
redirect action:"index", method:"GET"
}
'*'{ render status: NO_CONTENT }
}
Notice that for an HTML response a redirect is issued back to the index action, whilst for other content types a response code 204 (NO_CONTENT) is returned.
10.5.3 Generating a REST controller using scaffolding
To see some of these concepts in action and help you get going, the Scaffolding plugin, version 2.0 and above, can generate a REST ready controller for you, simply run the command:
$ grails generate-controller <<Domain Class Name>>
10.6 Calling REST Services with HttpClient
Calling Grails REST services - as well as third-party services - is very straightforward using the Micronaut HTTP Client. This HTTP client has both a low-level API and a higher level AOP-driven API, making it useful for both simple requests as well as building declarative, type-safe API layers.
To use the Micronaut HTTP client you must have the micronaut-http-client and micronaut-serde-jackson dependencies on your classpath. Add the following dependency to your build.gradle file.
implementation "io.micronaut:micronaut-http-client:4.6.6"
implementation "io.micronaut.serde:micronaut-serde-jackson:2.11.0"
Low-level API
The HttpClient interface forms the basis for the low-level API. This interfaces declares methods to help ease executing HTTP requests and receive responses.
The majority of the methods in the HttpClient interface returns Reactive Streams Publisher instances, and a sub-interface called RxHttpClient is included that provides a variation of the HttpClient interface that returns RxJava Flowable types. When using HttpClient in a blocking flow, you may wish to call toBlocking() to return an instance of BlockingHttpClient.
There are a few ways by which you can obtain a reference to a HttpClient. The most simple way is using the create method
List<Album> searchWithApi(String searchTerm) {
String baseUrl = "https://itunes.apple.com/"
BlockingHttpClient client = HttpClient.create(baseUrl.toURL()).toBlocking() (1)
HttpRequest request = HttpRequest.GET("/search?limit=25&media=music&entity=album&term=${searchTerm}")
HttpResponse<String> resp = client.exchange(request, String)
client.close() (2)
String json = resp.body()
ObjectMapper objectMapper = new ObjectMapper() (3)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
SearchResult searchResult = objectMapper.readValue(json, SearchResult)
searchResult.results
}
| 1 | Create a new instance of HttpClient with the create method, and convert to an instance of BlockingHttpClient with toBlocking(), |
| 2 | The client should be closed using the close method to prevent thread leaking. |
| 3 | Jackson’s ObjectMapper API can be used to map the raw JSON to POGOs, in this case SearchResult |
Consult the Http Client section of the Micronaut user guide for more information on using the HttpClient low-level API.
Declarative API
A declarative HTTP client can be written by adding the @Client annotation to any interface or abstract class. Using Micronaut’s AOP support (see the Micronaut user guide section on Introduction Advice), the abstract or interface methods will be implemented for you at compilation time as HTTP calls. Declarative clients can return data-bound POGOs (or POJOs) without requiring special handling from the calling code.
package example.grails
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@Client("https://start.grails.org")
interface GrailsAppForgeClient {
@Get("/{version}/profiles")
List<Map> profiles(String version)
}
Note that HTTP client methods are annotated with the appropriate HTTP method, such as @Get or @Post.
To use a client like the one in the above example, simply inject an instance of the client into any bean using the @Autowired annotation.
@Autowired GrailsAppForgeClient appForgeClient
List<Map> profiles(String grailsVersion) {
respond appForgeClient.profiles(grailsVersion)
}
For more details on writing and using declarative clients, consult the Http Client section of the Micronaut user guide.
Spring HTTP Interface Client
Spring Framework 7 can create an HTTP client from an interface annotated with @HttpExchange and @GetExchange. This is a Spring API, not a Grails DSL or a controller contract.
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
@HttpExchange("/books")
public interface BookClient {
@GetExchange("/{id}")
Book findById(@PathVariable Long id);
}
For a synchronous client, create a proxy with RestClient:
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
RestClient restClient = RestClient.builder()
.baseUrl("https://example.com")
.build();
BookClient bookClient = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(BookClient.class);
Spring 7 can also register the proxy as a bean. Group related interfaces and configure their shared RestClient builder:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.HttpServiceGroup.ClientType;
import org.springframework.web.service.registry.ImportHttpServices;
@Configuration
@ImportHttpServices(group = "books", types = BookClient.class,
clientType = ClientType.REST_CLIENT)
class BookClientConfiguration {
@Bean
RestClientHttpServiceGroupConfigurer bookClientConfigurer() {
return groups -> groups.filterByName("books")
.forEachClient((group, builder) -> builder.baseUrl("https://example.com"));
}
}
The registered BookClient can then be constructor-injected into an application bean and called like any other interface. Synchronous methods can return a decoded body, HttpHeaders, or ResponseEntity.
For Mono or Flux return values, add Spring WebFlux to the classpath and use WebClient instead. Register the group with clientType = ClientType.WEB_CLIENT and configure it through WebClientHttpServiceGroupConfigurer; reactive applications also need Reactor types on the classpath.
HTTP interface parameters cannot be null unless the parameter is optional. By default, 4xx and 5xx responses raise an exception. Configure status handling on the underlying RestClient or WebClient when an API requires different behavior.
10.7 The REST Profile
Grails supports a tailored profile for creating REST applications that provides a more focused set of dependencies and commands.
To get started with the REST profile using the Grails Shell CLI, create an application specifying rest-api as the name of the profile:
$ grails create-app my-api --profile rest-api
This will create a new REST application that provides the following features:
-
Default set of commands for creating and generating REST endpoints
-
Defaults to using JSON views for rendering responses (see the next section)
-
Fewer plugins than the default Grails web profile (no GSP, no Asset Pipeline, nothing HTML related)
You will notice for example in the grails-app/views directory that there are *.gson files for rendering the default index page and as well as any 404 and 500 errors.
If you issue the following set of commands:
$ grails create-domain-class my.api.Book
$ grails generate-all my.api.Book
Instead of CRUD HTML interface a REST endpoint is generated that produces JSON responses. In addition, the generated functional and unit tests by default test the REST endpoint.
To generate a REST application using the Forge CLI, use the following:
$ grails -t forge create-restapi my-api
This will create a new REST application with the same focused setup as above:
-
Default set of commands for creating and generating REST endpoints
-
Defaults to using JSON views for rendering responses (see the next section)
-
Fewer plugins than a default Grails Web-style application (no GSP, no Asset Pipeline, nothing HTML related)
You will notice for example in the grails-app/views directory that there are *.gson files for rendering the default index page and as well as any 404 and 500 errors.
If you issue the following set of commands:
$ grails create-domain-class my.api.Book
$ ./gradlew runCommand -Pargs="generate-all my.api.Book"
The generate-* commands are only available after adding the org.apache.grails:grails-scaffolding dependency to your project. They are not available by default in a REST application. Also, they will no longer produce *.gson files as that was a feature of the REST API-profile.
|
Instead of CRUD HTML interface a REST endpoint is generated that produces JSON responses. In addition, the generated functional and unit tests by default test the REST endpoint.
10.8 OpenAPI Descriptions
Grails applications can publish an OpenAPI description of their REST
endpoints. The optional grails-openapi module derives it from the application itself - its URL
mappings, its controllers, its domain classes and command objects, and their constraints - and
lets the standard OpenAPI annotations correct or enrich what is derived.
The description can be produced two ways, from the same configuration:
-
At build time, by the
generate-open-apicommand, which writes it to a file. The file can be packaged and served as a static resource, reviewed in a change, or handed to a client code generator, and the running application exposes nothing it does not choose to serve. -
At runtime, by springdoc-openapi, which serves it at
/v3/api-docsand can serve Swagger UI beside it. springdoc builds its document from Spring MVC handler methods, which Grails does not use, so the module contributes the Grails endpoints to it.
Getting Started
dependencies {
implementation 'org.apache.grails:grails-openapi'
}
That is all the build-time command needs. To serve the description at runtime as well, add springdoc, and optionally Swagger UI:
dependencies {
// the document, served at /v3/api-docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api'
// or the document and Swagger UI, served at /swagger-ui/index.html
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui'
}
Generating the Description at Build Time
./gradlew generateOpenApi
The command starts the application context, without a web server, and writes the default document
to build/openapi/openapi.yaml and each group to build/openapi/openapi-<group>.yaml.
A character of the group’s name a file name cannot hold, such as the / of admin/v1, is written
as -, so that group is written to openapi-admin-v1.yaml; where two groups would then be written
to one file, the command writes nothing and names them.
The --output-directory and --format options, or the grails.openapi.output-directory and
grails.openapi.output-format settings, choose where and whether YAML or JSON is written:
./gradlew runCommand -Pargs="generate-open-api --format=json --output-directory=build/api"
Because the command starts the application, it needs whatever the application needs to start in the environment it runs in, such as a datasource.
Where springdoc is configured, the command applies the method filters, operation customizers and
OpenAPI customizers springdoc applies to the documents it serves, so each file describes what
springdoc serves. That holds only in an environment where springdoc is enabled: with
springdoc.api-docs.enabled: false, as an application generated with the openapi feature has in
production, springdoc configures nothing, so the command writes no springdoc.group-configs group and
applies none of springdoc’s filters and customizers, and says so; grails.openapi.groups are
written as ever. A filter or customizer that fails there, such as one that reads the current
request, is skipped and logged, rather than the operations it was applied to.
To package the description and serve it as a static file, include it in the application’s static resources when the application is assembled:
def openApi = layout.buildDirectory.dir('openapi')
tasks.named('bootJar') {
dependsOn 'generateOpenApi'
from(openApi) { into 'BOOT-INF/classes/static/api' }
}
It is then served at /static/api/openapi.yaml, and any viewer can render it. For example,
Redoc needs only a page that points at the file:
<redoc spec-url="/static/api/openapi.yaml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
Serving the Description at Runtime
With springdoc on the classpath no further configuration is required. springdoc registers its own resource handlers, so Swagger UI is served even though Grails leaves Spring Boot’s catch-all resource handler disabled, and the springdoc paths fall through the Grails URL mappings because they resolve to no controller.
The Grails operations are added to each document before any other springdoc customizer runs, so an
OpenApiCustomizer bean, a GlobalOpenApiCustomizer bean, or a customizer a GroupedOpenApi adds
sees them and can change them. springdoc runs an OpenApiLocaleCustomizer before all of those,
though, so one does not see the Grails operations, and the generate-open-api command, which
answers no request in a locale, does not run one.
springdoc describes the application’s own Spring MVC endpoints as it does without Grails, since Jackson rather than Grails renders what they return: a domain class one of them returns is described with everything Jackson writes of it, such as a getter with nothing persisted behind it, and with the whole of what it is associated with. Where a Grails endpoint in the same document uses the class too, it is described once, as Grails renders it, and the Spring MVC endpoint refers to that schema as well. A class springdoc describes under the same name as another the Grails endpoints use is named apart from it, as below, in each document on its own.
An application whose URL mappings include a catch-all that resolves to a view or a controller - a
single page application forwarding unmatched paths to an index view, or an API answering unmatched
paths with a 404 of its own - needs nothing more either: the paths springdoc serves are excluded
from the URL mappings, at springdoc.api-docs.path and springdoc.swagger-ui.path where they are
configured, while springdoc serves them.
The runtime description is served to anyone who can reach the application, and describes
every endpoint, resource and constraint it documents. Secure /v3/api-docs/ and
/swagger-ui/ the way the application secures anything else, or disable them where they should
not be served, for example in production with springdoc.api-docs.enabled: false and
springdoc.swagger-ui.enabled: false, which an application generated with the openapi feature
sets for production. An application that only needs the description as a file
can generate it at build time and leave springdoc out altogether. With the Spring Security plugin,
whose default is to reject a request no rule matches, the paths need a rule of their own, such as
an entry in grails.plugin.springsecurity.controllerAnnotations.staticRules.
|
Configuration
| Setting | Default | Meaning |
|---|---|---|
|
|
Whether the description is generated at all |
|
A YAML or JSON document the description starts from, see The Base Document |
|
|
|
The title of the default document |
|
|
Whether only annotated actions are described, see Choosing What Is Described |
|
|
Whether the |
|
Ant patterns of the paths the default document describes, or leaves out |
|
|
The packages of the controllers the default document describes, or leaves out |
|
|
The media types an operation the default document describes must produce, or consume |
|
|
The header conditions an operation must declare, such as |
|
|
A group and what it selects, see Groups |
|
|
|
Where |
|
|
Whether |
Where springdoc is used, its own springdoc.paths-to-match, springdoc.paths-to-exclude,
springdoc.packages-to-scan, springdoc.packages-to-exclude, springdoc.produces-to-match,
springdoc.consumes-to-match and springdoc.headers-to-match apply to the Grails endpoints of the
default document too, and springdoc.api-docs.version chooses between OpenAPI 3.1, the default,
and 3.0 for both the build-time and the runtime description.
Choosing What Is Described
By default every action a URL mapping reaches is described. An application whose published API is only part of what it serves can narrow that down:
-
@Hiddenon a controller or an action, or@Operation(hidden = true), withholds it. -
grails.openapi.annotated-only: truedescribes only an action that declares@Operation, or whose controller declares@Tag, so the published API is exactly what the application annotated. -
The path and package settings above select by where an endpoint is served and which controller serves it:
grails:
openapi:
paths-to-match: /api/**
packages-to-exclude: com.example.internal
The media type settings select the way springdoc selects a Spring MVC handler method: an operation
is described only where it produces, or consumes, exactly the media types a setting lists. An
operation produces the media types of its controller’s responseFormats, application/json where
it declares none, and consumes the same media types where it binds a body. A mapping declared for a
version declares the Accept-Version header it is matched on, so headers-to-match:
Accept-Version=1.0 selects the operations of version 1.0. Any other header criterion leaves every
Grails endpoint out, as it leaves out a Spring handler method that declares no header condition:
grails:
openapi:
produces-to-match: application/json
Where springdoc is used, its method filters decide for the Grails actions too. Each filter is
called with the method the action is declared as, and an action a filter excludes is left out, at
runtime and by the generate-open-api command. An OpenApiMethodFilter bean applies to the default
document, a GlobalOpenApiMethodFilter bean to every document, and a filter a GroupedOpenApi adds
to that group:
@Bean
OpenApiMethodFilter internalActionFilter() {
{ Method action -> !action.isAnnotationPresent(Internal) } as OpenApiMethodFilter
}
The create and edit actions of a RestfulController answer the forms an HTML client renders,
so they are left out unless grails.openapi.include-form-actions is set.
Groups
A group is a document of its own, describing part of the application for one audience:
grails:
openapi:
groups:
sales:
display-name: Sales API
paths-to-match: /api/v1/**
depots:
display-name: Depot Lifecycle API
paths-to-match: /api/v2/**
Each group takes the same paths-to-match, paths-to-exclude, packages-to-scan,
packages-to-exclude, produces-to-match, consumes-to-match and headers-to-match settings as
the default document, and display-name titles it. The
generate-open-api command writes each group to a file of its own, and at runtime springdoc serves
each at /v3/api-docs/<name>, applying the same criteria to any Spring MVC endpoints the
application also has. A group declared to springdoc, in springdoc.group-configs or as a
GroupedOpenApi bean, is honored the same way, both at runtime and by the command. Where springdoc
is used, its own top-level criteria, such as springdoc.paths-to-match, apply to every group ahead
of the group’s own, criterion by criterion, as springdoc applies them to its Spring MVC endpoints. A
group name declared more than once is described by its first declaration, and logged, since
springdoc may select the group’s Spring MVC endpoints by another:
springdoc:
group-configs:
- group: sales
paths-to-match: /api/v1/**
The Base Document
What describes the API as a whole - its title and description, its servers, how it is secured, the descriptions of its tags, vendor extensions for a viewer, and endpoints the application does not map itself, such as a login endpoint a security plugin provides - is written once, in a base document the description starts from:
grails:
openapi:
base-document: classpath:openapi-base.yml
openapi: 3.1.0
info:
title: Sales API
description: |
Inventory, orders and invoices for sales customers.
servers:
- url: https://api.example.com
security:
- Bearer: []
tags:
- name: orders
description: Retrieving and creating orders
components:
securitySchemes:
Bearer:
type: http
scheme: bearer
bearerFormat: JWT
The information, servers, security and external documentation of the base document are used as they
are. Its tags, extensions and components are kept beside what is derived, and so are its paths, in
each document whose path settings select them. Where the base
document declares no version, the application’s info.app.version is used, and without a base
document the description is titled with info.app.name.
What Is Documented
Every URL mapping that names a controller statically becomes a path. A resources mapping
contributes each of its generated HTTP methods:
class UrlMappings {
static mappings = {
"/books"(resources: 'book')
}
}
produces GET and POST on /books, and GET, PUT, PATCH and DELETE on /books/{id}, with
POST, which Grails maps to update as well, where the controller’s allowedMethods allows it.
A RestfulController constructed read only answers 405 from its write actions, so only what it
serves is described:
class PublisherController extends RestfulController<Publisher> {
PublisherController() {
super(Publisher, true)
}
}
mapped with "/publishers"(resources: 'publisher'), produces only GET on /publishers and
/publishers/{id}. A write action the controller overrides
is described, because it does whatever the override does.
URL variables become OpenAPI path parameters, so "/books/$id" is documented as /books/{id}. The
optional .format extension Grails appends to REST mappings is omitted, because OpenAPI describes
response formats through content types rather than the path. An OpenAPI path parameter is always
required, so a mapping with an optional variable, such as "/photos/$id?", is documented at both
/photos/{id} and /photos. A mapping with a wildcard that captures nothing, such as a catch-all
"/**", cannot be written as an OpenAPI path and is not documented.
A mapping that names a controller but no action is documented as the controller’s default action.
One that takes the action from the path, such as "/topics/$action"(controller: 'topic'), is
documented for each action the controller declares, and, where the action is optional, as
"/topics/$action?" makes it, at the path without it as the controller’s default action. One that
chooses the action by the method of the request, such as action: [GET: 'show', DELETE: 'delete'],
is documented for each method. A mapping whose action is decided by a closure as each request is
made is skipped, and logged.
A mapping declared for an HTTP method is documented for that method, unless the controller’s
allowedMethods refuses it for the action, which Grails answers with 405. A mapping that accepts
any method is documented for the methods allowedMethods declares for the action, or, where it
declares none, for the method a RestfulController action of that name answers, and GET for any
other action.
A mapping that names a namespace is documented with that namespace’s controller, so versioned
controllers of the same name are each described at their own routes.
Versions
A mapping declared for a version is matched on the Accept-Version header a request sends:
"/books"(version: '1.0', resources: 'book', namespace: 'v1')
"/books"(version: '2.0', resources: 'book', namespace: 'v2')
A request asking for no version is answered by the highest version, so that is the one the default
document describes, with an optional Accept-Version parameter naming it. Grails also takes the
version from a v parameter of the Accept media type, which is not described. A group describes another
version by selecting its header, and there the parameter is required:
grails:
openapi:
groups:
v1:
headers-to-match: Accept-Version=1.0
Resource Schemas
A RestfulController is described as serving the type it declares: BookController extends
RestfulController<Book> responds with, and accepts, a Book, whatever the controller is named.
The type may be a domain class or any other class, such as a command object the controller renders
in place of the domain class. A controller that is not a RestfulController declares no response
type, so its responses are described without a body unless an annotation declares one.
A type is described in components/schemas through swagger-core, so a @Schema or Jackson
annotation on the class or on one of its properties is honored. The metaClass every Groovy object
has and the errors a validateable object has are not part of it.
A property of a class the Grails operations are described with - a domain class, a command object,
or any other class, such as one an @ApiResponse names - is described by the name Grails'
converters, JSON views and data binding render and bind it by: its own. So neither a
@JsonProperty rename, which Grails ignores, nor the name Jackson would otherwise write, such as
isbn for ISBN, changes it, and a command object’s request parameters are described by the names
Grails binds them by. Where the description should name a property otherwise, @Schema(name)
renames it, and its constraints, and whether it is required or read only, follow the rename. A
class only springdoc’s own Spring MVC endpoints use keeps the names Jackson writes, since Jackson
renders it there.
A schema is named after its class, or after the name a @Schema annotation on the class declares.
Classes sharing a name in different packages are each named by their package as well, such as
com.example.v1.Book and com.example.v2.Book, so neither stands in for the other and the names do
not depend on which is described first. A class sharing the name of a schema the base document
declares, or one the document derives, such as ValidationErrors or a Patch schema, is named by
its package in the same way. Only a class described as a schema of its own takes a name: an enum is
described where it is used unless @Schema(enumAsRef = true) describes it as a schema of its own,
and a class described in the place of another - the implementation a @Schema annotation names, or
the value a @JsonValue accessor returns - leaves the name to the class described.
import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = 'A book in the catalog')
class Book {
@Schema(description = 'Full title as printed', example = 'Dune')
String title
static constraints = {
title blank: false, nullable: false, maxSize: 255
}
}
The declared constraints of a domain class or a Validateable class are applied over that result,
because swagger-core cannot see them. The two combine, so the annotation above supplies the prose
and the constraints block the validation:
"title": {
"type": "string",
"description": "Full title as printed",
"example": "Dune",
"maxLength": 255
}
The constraints are carried across as follows:
| Constraint | OpenAPI |
|---|---|
|
listed in |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Where the annotation and the constraints both say whether a property must be sent, the property’s
@Schema wins: a property it declares nullable = true or
requiredMode = NOT_REQUIRED is not listed in required, and one it declares
requiredMode = REQUIRED is, whatever it is constrained to. So a Validateable class that declares
no constraints, whose every property Grails constrains nullable: false by default, still describes
the properties it annotates as optional that way.
A domain class is described as Grails renders it: its identifier and its persistent properties. A
transient, or a getter with nothing persisted behind it, is not rendered and is not described. The
version is not rendered either unless it is configured to be, and is described only then: in JSON
with grails.converters.json.domain.include.version, or grails.converters.domain.include.version
for every format. The document describes the version where JSON renders it, so a version rendered in
XML alone, with grails.converters.xml.domain.include.version, is not described.
A property data binding does not bind is marked readOnly: the identifier and the version, which the
server assigns, dateCreated and lastUpdated, a property constrained bindable: false, and a
property a command object can only read. Tools that understand OpenAPI leave a read-only property
out of a request body, so one schema describes both directions.
A property described by a reference to another schema, such as an embedded class, keeps what is said
of it, being nullable, whether its constraint or its @Schema says so, or read only, or the
description, deprecation or access mode @Schema gives it. OpenAPI 3.1 says so beside the $ref, and a nullable one is oneOf the reference and a null
type. OpenAPI 3.0 ignores anything beside a $ref, so there the property is allOf the reference,
and says it on that.
An association to another domain class is described by the associated identifier, which is what Grails renders for it and what it binds it from:
"author": {
"type": "object",
"properties": { "id": { "type": "integer", "format": "int64" } },
"required": ["id"]
}
A to-many association is an array of those references, and an embedded association is described in full.
A class that cannot be introspected is left out of the document and logged, rather than failing the document, and an operation that referred to it is described without a shape rather than with a reference that does not resolve. A class whose constraints cannot be evaluated is described without them, and logged.
Request Bodies
An operation that accepts a body describes the type the action binds. Where the action takes a command object that is the command, in preference to the resource the controller serves:
class OrdersController {
def submit(OrderCommand cmd) { }
}
Where one of the actions RestfulController declares takes no command object, the body is the
resource the controller serves, whether the controller inherits the action or overrides it. Any other
action binds a body only where it takes a command object or declares one. A patch binds only what it is sent, so its body is a copy of the resource’s
schema, named for it with a Patch suffix, that requires nothing. A body with a property a file is
bound to, a MultipartFile or a Part, is described as multipart/form-data, with the file as a
binary string. An action that binds something
else declares it with @RequestBody, which replaces what is derived.
Responses
The actions RestfulController declares are described by the part each plays in the resource, so
the statuses are the ones it answers with rather than a uniform 200. That holds for an action the
controller overrides as much as for one it inherits: an override is how an action is annotated, and
it still lists, shows, creates, updates or deletes the resource:
| Action | Status | Body |
|---|---|---|
|
200 |
a collection of the resource |
|
200, 404 |
the resource |
|
200 |
the resource |
|
201, 422 |
the resource |
|
200, 404, 422 |
the resource |
|
204, 404 |
none |
Any other action of the controller, such as a search, is described as any controller’s action is:
a 200, a 404 where its path has a variable, and whatever its annotations declare.
A 422 answers with the validation errors Grails renders for a request that cannot be bound,
described once as the ValidationErrors schema, or as grails.validation.ValidationErrors, after
the class Grails renders them from, where the document already describes something else as
ValidationErrors, such as a class of the application a Spring MVC endpoint returns. The
converters render them, in JSON as an object
listing them and in XML as an errors element holding an error element for each. Where a JSON
view renders them, as the errors view of an application generated with JSON views does, they are
described in JSON as that view renders them: one error with its message, path and link, or several
embedded with their total. The status is the one the view sets, which is 422 for the errors view
an application is generated with. So:
-
A controller with an errors view of its own, such as
grails-app/views/book/_errors.gson, is described answering422in JSON without a shape, since only the application knows it; the view answers with the status it sets, as the errors view it is usually copied from sets422. -
An application with JSON views but no errors view renders the errors with the view for any object, which answers with success rather than
422, so its422is described in XML alone. -
An application rendering its errors another way declares
ValidationErrorsin the base document, which then describes them in every media type, or describes an action’s errors with@ApiResponse.
A response and a request body are described in the media types of the formats the controller
declares in responseFormats, for the action or for every action, and in application/json where
it declares none. A format is described as the first media type the application configures for it
in grails.mime.types:
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml']
}
describes each body as application/json and as text/xml, the first media type Grails configures
for xml by default. Only a data format, json or xml, carries the shape described; a format that
renders a view, a form or a HAL document, such as html or hal, is listed without one. A request
body is described in the data formats alone.
The XML is described as the converters render it, with the OpenAPI xml object: a domain class or a
command object as an element named for its class, such as book, its identifier as the id
attribute and its version as an attribute, an association as an element carrying the associated
identifier as the id attribute, a collection of values as an element holding one named for the
class of each, such as <lines><string>a</string></lines>, and a listing as a list element
holding one for each. The xml object cannot describe a map, which Grails renders as entry
elements keyed by an attribute, and the document describes the XML Grails renders by default, not
that of grails.converters.xml.default.deep or grails.converters.xml.domain.include.class.
RestfulController answers its own save and update with a Location header naming the
resource, which the 201 and 200 responses describe, and its patch too, which update serves.
The header is something only RestfulController’s code sends, rather than part of what the action
answers by its role, since a controller generated for a REST application does not send it. So an
action the controller overrides is described without it, and one that sends it declares it with
`@ApiResponse(headers).
Parameters
A listing accepts the paging and sorting RestfulController passes to GORM, so those are described
on it:
| Parameter | Meaning |
|---|---|
|
the most results to return, capped at 100, defaulting to 10 |
|
the result to start from |
|
the property to sort by |
|
|
An index the controller overrides is described with them too. One that does not page withdraws
them, as it would any parameter derived for it:
@Parameter(name = 'max', hidden = true)
@Parameter(name = 'offset', hidden = true)
@Parameter(name = 'sort', hidden = true)
@Parameter(name = 'order', hidden = true)
@Override
Object index(Integer max) {
respond bookService.featured()
}
The identifier an operation of a RestfulController addresses is described with the type the
domain class declares for it:
| Identifier | OpenAPI |
|---|---|
|
|
|
|
|
|
|
|
An association to the domain class is described with the same type. A @Parameter declared for the
identifier refines it, and keeps that type unless its schema names another.
An action parameter of a simple type is bound from the request by name, so it is described as a query parameter of that type:
def lookup(String query, Integer limit) { }
This needs the parameter names, which Grails keeps by default through the preserveParameterNames
setting of the Grails Gradle plugin. A parameter renamed with @RequestParameter is described by the
name a request sends. A parameter an action reads from params rather than declaring is described
with @Parameter. A parameter Grails does not bind - one declared as Object, an interface or an
abstract class - is not described.
A command object an action binds on a request without a body, such as a GET, is bound from the
request parameters, so each property it binds that a request parameter can carry is described as a
query parameter, with its constraints:
def search(BookSearch search) { }
A path variable is described with the type of what it is bound to: the identifier of the resource
an operation addresses, the identifier of the resource a nested mapping names it by, such as
bookId in /books/{bookId}/chapters, or else the action parameter of the same name. A pattern or
a list of values the mapping constrains it to is described too:
"/isbn/$code"(controller: 'book', action: 'byIsbn') {
constraints {
code(matches: /\d{13}/)
}
}
Correcting What Is Derived
What is derived can be corrected or enriched with the standard OpenAPI annotations. They are read by swagger-core, so they mean what they mean on any other endpoint:
| Annotation | Effect |
|---|---|
|
the summary, description, operation id, tags, deprecation, external documentation, parameters, responses, request body and security of an action |
|
a response of an action, or, on the controller, of each of its actions; one declared for a status the operation already describes replaces it, and a success status an action declares replaces the success statuses derived for it |
|
a parameter of an action, declared on the action or on one of its parameters; one the operation already describes, such as a path variable, is refined |
|
the body an action binds |
|
the security of an action, or, on the controller, of each of its actions |
|
the groups a controller’s operations appear under, and their descriptions |
|
withholds a controller or an action |
An action answers success with the statuses it declares, so a success status declared on the action,
or in its @Operation, replaces any other success status derived for it, with what that said, such
as a Location header. A save that queues what it is sent, rather than creating it, and declares
@ApiResponse(responseCode = '200', description = 'Queued') is described answering 200, not
201. An error status an action declares is added to those derived, and a status its controller
declares is added to each of its actions, and kept beside the success status an action declares.
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
@Tag(name = 'orders', description = 'Retrieving and creating orders')
@ApiResponse(responseCode = '401', description = 'Invalid API token')
@ApiResponse(responseCode = '403', description = 'Permission denied')
class OrderController extends RestfulController<OrderCommand> {
OrderController() {
super(OrderCommand)
}
@Operation(summary = 'Fetch all orders')
@Parameter(name = 'profile', in = ParameterIn.QUERY,
description = 'The customer profile to act for',
schema = @Schema(implementation = Long))
@Override
Object index(Integer max) {
// ...
}
@ApiResponse(responseCode = '200', description = 'The CSC letter',
content = @Content(mediaType = 'application/pdf',
schema = @Schema(type = 'string', format = 'binary')))
def cscLetter() {
// ...
}
}
Where springdoc is used, its operation customizers are given each Grails operation too, once the
annotations are applied, with a HandlerMethod of the controller and the method the action is
declared as: an OperationCustomizer bean for the default document, a GlobalOperationCustomizer
bean for every document, and a customizer a GroupedOpenApi adds for that group. A customizer that
returns null leaves the operation out. A schema a customizer adds to the components is kept under
the name it gives it, and is not named apart from the classes the Grails operations are described
with. The generate-open-api command applies them the same way:
@Bean
GlobalOperationCustomizer actionNameCustomizer() {
{ Operation operation, HandlerMethod handlerMethod ->
operation.addExtension('x-action', "${handlerMethod.beanType.simpleName}.${handlerMethod.method.name}")
operation
} as GlobalOperationCustomizer
}
Controllers a Mapping Reaches Without Naming
A REST application maps its controllers without naming them, either by naming the action and leaving the controller to the request, as a generated REST API does:
get "/$controller(.$format)?"(action: 'index')
post "/$controller(.$format)?"(action: 'save')
get "/$controller/$id(.$format)?"(action: 'show')
or by leaving both, as the default mapping does:
"/$controller/$action?/$id?(.$format)?" {}
Either way the REST controllers those mappings reach are described, at the URLs the mapping serves:
RestfulController subclasses, and controllers declaring the formats they respond in with
responseFormats, none of them html. The first form above produces GET /book, POST /book and GET /book/{id}; the
second produces GET /book/index, POST /book/save and GET /book/show/{id}, and, because its
action is optional, GET /book for the controller’s default action. An application whose
mappings name every controller gets neither, because expansion follows the mappings rather than the
controllers.
A mapping that captures the namespace, such as "/$namespace/$controller/$action?/$id?", reaches
each controller at its own namespace, and does not reach a controller without one. A mapping that
leaves the namespace out reaches the controller Grails resolves for the name alone, so of two
controllers sharing a name only a namespaced mapping reaches either.
A controller a resources mapping names and a mapping that does not name it are described at both,
because both answer. Where that gives two operations the same identifier the second is qualified by
the path it is reached at, so no two operations in the document share one. An application wanting
only the first form removes the default mapping from its UrlMappings.
A controller rendering views for a browser declares no responseFormats, or declares html among
them, such as a login controller responding in html and json, so it does not appear in the
document. Where responseFormats is a map by action, each action is decided by its own entry: with
[index: ['json'], page: ['html', 'json']], index is described and page, like an action the map
leaves out, is not. responseFormats is read as respond reads it, a list, or a list for each action;
an array is not one, so respond answers in any configured format, html too. Controllers @Scaffold generates extend RestfulController, and those generate-controller
generates for a REST application declare responseFormats, so both are described; those it
generates for a web application render views and are not.
A controller that is not a RestfulController but whose save and update actions bind the same
domain class, as the controllers generated for a REST application do, answers the actions
RestfulController declares as a RestfulController of that class does, so they are described the
same way: with its statuses, the domain class as its responses and bodies, and the paging of its
listing. Its other actions are described as any controller’s are.
Limitations
-
A mapping that accepts any HTTP method for an action
allowedMethodsdoes not restrict is documented asGET, because OpenAPI requires a concrete operation. -
A mapping declared for an HTTP method OpenAPI has no operation for is skipped, and logged.
-
A
RestfulControlleris known to be read only, and the resource it serves where it declares no type argument, from the controller the application serves requests with, which is not created to be described. So one in a scope other than singleton is described with its write actions, and one that also passes its resource only to the constructor, asextends RestfulControllerwithsuper(Book)does, is described without a schema for its requests and responses. This applies to every controller wheregrails.controllers.defaultScopeisprototype, rather than only to one declaring its own scope. Declaring the type argument, asextends RestfulController<Book>, describes the resource in any scope. -
A resource is described as Grails renders a domain class or an object by default. A JSON view, other than the errors view, or a custom renderer that renders another shape is described with
@ApiResponse. -
A date is described as a
date-time. Grails binds it with the formatsgrails.databinding.dateFormatslists, which by default read the offset orZof a time only when it has three digits of milliseconds, such as2024-05-01T10:00:00.000+02:00, or an offset written without a colon, such as2024-05-01T10:00:00+0200. Otherwise, as in2024-05-01T10:00:00Zor2024-05-01T10:00:00+02:00, the time is read in the zone of the server, and fewer digits of milliseconds are misread:2024-05-01T10:00:00.5Zis read as 5 milliseconds. -
A variable capturing several segments, such as
$path**, is described as a path parameter, which OpenAPI allows only one segment; a client must not encode the/it holds.
10.9 JSON Views
As mentioned in the previous section the REST profile by default uses JSON views to render JSON responses. These play a similar role to GSP, but instead are optimized for outputing JSON responses instead of HTML.
You can continue to separate your application in terms of MVC, with the logic of your application residing in controllers and services, whilst view related matters are handled by JSON views.
JSON views also provide the flexibility to easily customize the JSON presented to clients without having to resort to relatively complex marshalling libraries like Jackson or Grails' marshaller API.
| Since Grails 3.1, JSON views are considered by the Grails team the best way to present JSON output for the client, the section on writing custom marshallers has been removed from the user guide. If you are looking for information on that topic, see the Grails 3.0.x guide. |
10.9.1 Getting Started
If you are using the REST application or REST or AngularJS profiles, then the JSON views plugin will already be included and you can skip the remainder of this section. Otherwise you will need to modify your build.gradle to include the necessary plugin to activate JSON views:
implementation 'org.apache.grails:grails-views-gson:7.0.0' // or whatever is the latest version
| The source code repository for JSON views can be found on Github if you are looking for more documentation and contributions |
In order to compile JSON views for production deployment you should also activate the Gradle plugin by first modifying the buildscript block:
buildscript {
...
dependencies {
...
classpath platform("org.apache.grails:grails-gradle-bom:{version}")
classpath "org.apache.grails:grails-gradle-plugins"
}
}
Then apply the org.apache.grails.views-json Gradle plugin after any Grails core gradle plugins:
...
apply plugin: "org.apache.grails.gradle.grails-web"
apply plugin: "org.apache.grails.views-json"
This will add a compileGsonViews task to Gradle, which is invoked prior to creating the production JAR or WAR file.
10.9.2 Creating JSON Views
JSON views go into the grails-app/views directory and end with the .gson suffix. They are regular Groovy scripts and can be opened in any Groovy editor.
Example JSON view:
json.person {
name "bob"
}
| To open them in the Groovy editor in Intellij IDEA, double click on the file and when asked which file to associate it with, choose "Groovy" |
The above JSON view produces:
{"person":{"name":"bob"}}
There is an implicit json variable which is an instance of StreamingJsonBuilder.
Example usages:
json(1,2,3) == "[1,2,3]"
json { name "Bob" } == '{"name":"Bob"}'
json([1,2,3]) { n it } == '[{"n":1},{"n":2},{"n":3}]'
Refer to the API documentation on StreamingJsonBuilder for more information about what is possible.
10.9.3 JSON View Templates
You can define templates starting with underscore _. For example given the following template called _person.gson:
model {
Person person
}
json {
name person.name
age person.age
}
You can render it with a view as follows:
model {
Family family
}
json {
name family.father.name
age family.father.age
oldestChild g.render(template:"person", model:[person: family.children.max { Person p -> p.age } ])
children g.render(template:"person", collection: family.children, var:'person')
}
Alternatively for a more concise way to invoke templates, using the tmpl variable:
model {
Family family
}
json {
name family.father.name
age family.father.age
oldestChild tmpl.person( family.children.max { Person p -> p.age } ] )
children tmpl.person( family.children )
}
10.9.4 Rendering Domain Classes with JSON Views
Typically your model may involve one or many domain instances. JSON views provide a render method for rendering these.
For example given the following domain class:
class Book {
String title
}
And the following template:
model {
Book book
}
json g.render(book)
The resulting output is:
{id:1, title:"The Stand"}
You can customize the rendering by including or excluding properties:
json g.render(book, [includes:['title']])
Or by providing a closure to add additional JSON output:
json g.render(book) {
pages 1000
}
10.9.5 JSON Views by Convention
There are a few useful conventions you can follow when creating JSON views. For example if you have a domain class called Book, then creating a template located at grails-app/views/book/_book.gson and using the respond method will result in rendering the template:
def show(Long id) {
respond Book.get(id)
}
In addition if an error occurs during validation by default Grails will try to render a template called grails-app/views/book/_errors.gson, otherwise it will try to render grails-app/views/errors/_errors.gson if the former doesn’t exist.
This is useful because when persisting objects you can respond with validation errors to render these aforementioned templates:
@Transactional
def save(Book book) {
if (book.hasErrors()) {
transactionStatus.setRollbackOnly()
respond book.errors
}
else {
// valid object
}
}
If a validation error occurs in the above example the grails-app/views/book/_errors.gson template will be rendered.
For more information on JSON views (and Markup views), see the JSON Views user guide.
10.10 Customizing Response Rendering
If you are looking for a more low-level API and JSON or Markup views don’t suite your needs then you may want to consider implementing a custom renderer.
10.10.1 Customizing the Default Renderers
The default renderers for XML and JSON can be found in the grails.rest.render.xml and grails.rest.render.json packages respectively. These use the Grails converters (grails.converters.XML and grails.converters.JSON) by default for response rendering.
You can easily customize response rendering using these default renderers. A common change you may want to make is to include or exclude certain properties from rendering.
Including or Excluding Properties from Rendering
As mentioned previously, Grails maintains a registry of grails.rest.render.Renderer instances. There are some default configured renderers and the ability to register or override renderers for a given domain class or even for a collection of domain classes. To include a particular property from rendering you need to register a custom renderer by defining a bean in grails-app/conf/spring/resources.groovy:
import grails.rest.render.xml.*
beans = {
bookRenderer(XmlRenderer, Book) {
includes = ['title']
}
}
| The bean name is not important (Grails will scan the application context for all registered renderer beans), but for organizational and readability purposes it is recommended you name it something meaningful. |
To exclude a property, the excludes property of the XmlRenderer class can be used:
import grails.rest.render.xml.*
beans = {
bookRenderer(XmlRenderer, Book) {
excludes = ['isbn']
}
}
Customizing the Converters
As mentioned previously, the default renders use the grails.converters package under the covers. In other words, under the covers they essentially do the following:
import grails.converters.*
...
render book as XML
// or render book as JSON
Why the separation between converters and renderers? Well a renderer has more flexibility to use whatever rendering technology you chose. When implementing a custom renderer you could use Jackson, Google Gson or any Java library to implement the renderer. Converters on the other hand are very much tied to Grails' own marshalling implementation.
Date and Time Rendering
The JSON converter renders date and time values the same way as Spring Boot’s default Jackson JsonMapper, so a Grails
application and a Spring Boot application return the same JSON for the same values:
| Type | Rendered as |
|---|---|
|
a UTC instant with millisecond precision, such as |
|
its wall-clock time in the JVM default time zone, such as |
|
ISO-8601, such as |
|
ISO-8601 without a time zone, such as |
|
ISO-8601 with the offset and without a zone ID, such as |
|
ISO-8601, such as |
|
a number, such as |
|
the ID, such as |
An OffsetTime keeps its seconds and writes a fraction of a second with only the digits it needs, as JSON views render
it, where Spring Boot writes "03:00-03:00". A map keyed by Date, Calendar or ZonedDateTime values renders its
keys as those values render by default, such as
"2025-10-08T07:48:46.407Z", also where grails.converters.json.date or a registered marshaller changes how the values
render; other map keys render with their toString(). JSON views render dates and times the same way.
A Month renders by its name, such as "SEPTEMBER", as every enum does, where Spring Boot writes its number. Data binding
binds a Month from either.
Setting grails.converters.json.date to javascript renders java.util.Date values as JavaScript new Date(…)
constructors instead. To render a type differently, register an object marshaller for it, for example in BootStrap.init.
This one renders a Month as its number, as Spring Boot does:
JSON.registerObjectMarshaller(Month) { Month month ->
month.value
}
10.10.2 Implementing a Custom Renderer
If you want even more control of the rendering or prefer to use your own marshalling techniques then you can implement your own Renderer instance. For example below is a simple implementation that customizes the rendering of the Book class:
package myapp
import grails.rest.render.*
import grails.web.mime.MimeType
class BookXmlRenderer extends AbstractRenderer<Book> {
BookXmlRenderer() {
super(Book, [MimeType.XML,MimeType.TEXT_XML] as MimeType[])
}
void render(Book object, RenderContext context) {
context.contentType = MimeType.XML.name
def xml = new groovy.xml.MarkupBuilder(context.writer)
xml.book(id: object.id, title:object.title)
}
}
The AbstractRenderer super class has a constructor that takes the class that it renders and the MimeType(s) that are accepted (via the ACCEPT header or file extension) for the renderer.
To configure this renderer, simply add it is a bean to grails-app/conf/spring/resources.groovy:
beans = {
bookRenderer(myapp.BookXmlRenderer)
}
The result will be that all Book instances will be rendered in the following format:
<book id="1" title="The Stand"/>
| If you change the rendering to a completely different format like the above, then you also need to change the binding if you plan to support POST and PUT requests. Grails will not automatically know how to bind data from a custom XML format to a domain class otherwise. See the section on "Customizing Binding of Resources" for further information. |
Container Renderers
A grails.rest.render.ContainerRenderer is a renderer that renders responses for containers of objects (lists, maps, collections etc.). The interface is largely the same as the Renderer interface except for the addition of the getComponentType() method, which should return the "contained" type. For example:
class BookListRenderer implements ContainerRenderer<List, Book> {
Class<List> getTargetType() { List }
Class<Book> getComponentType() { Book }
MimeType[] getMimeTypes() { [ MimeType.XML] as MimeType[] }
void render(List object, RenderContext context) {
....
}
}
10.10.3 Using GSP to Customize Rendering
You can also customize rendering on a per action basis using Groovy Server Pages (GSP). For example given the show action mentioned previously:
def show(Book book) {
respond book
}
You could supply a show.xml.gsp file to customize the rendering of the XML:
<%@page contentType="application/xml"%>
<book id="${book.id}" title="${book.title}"/>
10.11 Hypermedia as the Engine of Application State
HATEOAS, an abbreviation for Hypermedia as the Engine of Application State, is a common pattern applied to REST architectures that uses hypermedia and linking to define the REST API.
Hypermedia (also called Mime or Media Types) are used to describe the state of a REST resource, and links tell clients how to transition to the next state. The format of the response is typically JSON or XML, although standard formats such as Atom and/or HAL are frequently used.
10.11.1 HAL Support
HAL is a standard exchange format commonly used when developing REST APIs that follow HATEOAS principals. An example HAL document representing a list of orders can be seen below:
{
"_links": {
"self": { "href": "/orders" },
"next": { "href": "/orders?page=2" },
"find": {
"href": "/orders{?id}",
"templated": true
},
"admin": [{
"href": "/admins/2",
"title": "Fred"
}, {
"href": "/admins/5",
"title": "Kate"
}]
},
"currentlyProcessing": 14,
"shippedToday": 20,
"_embedded": {
"order": [{
"_links": {
"self": { "href": "/orders/123" },
"basket": { "href": "/baskets/98712" },
"customer": { "href": "/customers/7809" }
},
"total": 30.00,
"currency": "USD",
"status": "shipped"
}, {
"_links": {
"self": { "href": "/orders/124" },
"basket": { "href": "/baskets/97213" },
"customer": { "href": "/customers/12369" }
},
"total": 20.00,
"currency": "USD",
"status": "processing"
}]
}
}
Exposing Resources Using HAL
To return HAL instead of regular JSON for a resource you can simply override the renderer in grails-app/conf/spring/resources.groovy with an instance of grails.rest.render.hal.HalJsonRenderer (or HalXmlRenderer for the XML variation):
import grails.rest.render.hal.*
beans = {
halBookRenderer(HalJsonRenderer, rest.test.Book)
}
You will also need to update the acceptable response formats for the resource so that the HAL format is included. Not doing so will result in a 406 - Not Acceptable response being returned from the server.
This can be done by setting the formats attribute of the Resource transformation:
import grails.rest.*
@Resource(uri='/books', formats=['json', 'xml', 'hal'])
class Book {
...
}
Or by updating the responseFormats in the controller:
class BookController extends RestfulController<Book> {
static responseFormats = ['json', 'xml', 'hal']
// ...
}
With the bean in place requesting the HAL content type will return HAL:
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/hal+json;charset=ISO-8859-1
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "\"The Stand\""
}
To use HAL XML format simply change the renderer:
import grails.rest.render.hal.*
beans = {
halBookRenderer(HalXmlRenderer, rest.test.Book)
}
Rendering Collections Using HAL
To return HAL instead of regular JSON for a list of resources you can simply override the renderer in grails-app/conf/spring/resources.groovy with an instance of grails.rest.render.hal.HalJsonCollectionRenderer:
import grails.rest.render.hal.*
beans = {
halBookCollectionRenderer(HalJsonCollectionRenderer, rest.test.Book)
}
With the bean in place requesting the HAL content type will return HAL:
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 17 Oct 2013 02:34:14 GMT
{
"_links": {
"self": {
"href": "http://localhost:8080/books",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"book": [
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "The Stand"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Infinite Jest"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/3",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Walden"
}
]
}
}
Notice that the key associated with the list of Book objects in the rendered JSON is book which is derived from the type of objects in the collection, namely Book. In order to customize the value of this key assign a value to the collectionName property on the HalJsonCollectionRenderer bean as shown below:
import grails.rest.render.hal.*
beans = {
halBookCollectionRenderer(HalCollectionJsonRenderer, rest.test.Book) {
collectionName = 'publications'
}
}
With that in place the rendered HAL will look like the following:
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 17 Oct 2013 02:34:14 GMT
{
"_links": {
"self": {
"href": "http://localhost:8080/books",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"publications": [
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "The Stand"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Infinite Jest"
},
{
"_links": {
"self": {
"href": "http://localhost:8080/books/3",
"hreflang": "en",
"type": "application/hal+json"
}
},
"title": "Walden"
}
]
}
}
Using Custom Media / Mime Types
If you wish to use a custom Mime Type then you first need to declare the Mime Types in grails-app/conf/application.groovy:
grails.mime.types = [
all: "*/*",
book: "application/vnd.books.org.book+json",
bookList: "application/vnd.books.org.booklist+json",
...
]
Or the equivalent in grails-app/conf/application.yml:
grails:
mime:
types:
all: "*/*"
book: "application/vnd.books.org.book+json"
bookList: "application/vnd.books.org.booklist+json"
...:
| It is critical that place your new mime types after the 'all' Mime Type because if the Content Type of the request cannot be established then the first entry in the map is used for the response. If you have your new Mime Type at the top then Grails will always try and send back your new Mime Type if the requested Mime Type cannot be established. |
Then override the renderer to return HAL using the custom Mime Types:
import grails.rest.render.hal.*
import grails.web.mime.*
beans = {
halBookRenderer(HalJsonRenderer, rest.test.Book, new MimeType("application/vnd.books.org.book+json", [v:"1.0"]))
halBookListRenderer(HalJsonCollectionRenderer, rest.test.Book, new MimeType("application/vnd.books.org.booklist+json", [v:"1.0"]))
}
In the above example the first bean defines a HAL renderer for a single book instance that returns a Mime Type of application/vnd.books.org.book+json. The second bean defines the Mime Type used to render a collection of books (in this case application/vnd.books.org.booklist+json).
application/vnd.books.org.booklist+json is an example of a media-range (https://www.w3.org/Protocols/rfc2616/rfc2616.html - Header Field Definitions). This example uses entity (book) and operation (list) to form the media-range values but in reality, it may not be necessary to create a separate Mime type for each operation. Further, it may not be necessary to create Mime types at the entity level. See the section on "Versioning REST resources" for further information about how to define your own Mime types.
|
With this in place issuing a request for the new Mime Type returns the necessary HAL:
$ curl -i -H "Accept: application/vnd.books.org.book+json" http://localhost:8080/books/1
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/vnd.books.org.book+json;charset=ISO-8859-1
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/vnd.books.org.book+json"
}
},
"title": "\"The Stand\""
}
Customizing Link Rendering
An important aspect of HATEOAS is the usage of links that describe the transitions the client can use to interact with the REST API. By default the HalJsonRenderer will automatically create links for you for associations and to the resource itself (using the "self" relationship).
However you can customize link rendering using the link method that is added to all domain classes annotated with grails.rest.Resource or any class annotated with grails.rest.Linkable. For example, the show action can be modified as follows to provide a new link in the resulting output:
def show(Book book) {
book.link rel:'publisher', href: g.createLink(absolute: true, resource:"publisher", params:[bookId: book.id])
respond book
}
Which will result in output such as:
{
"_links": {
"self": {
"href": "http://localhost:8080/books/1",
"hreflang": "en",
"type": "application/vnd.books.org.book+json"
}
"publisher": {
"href": "http://localhost:8080/books/1/publisher",
"hreflang": "en"
}
},
"title": "\"The Stand\""
}
The link method can be passed named arguments that match the properties of the grails.rest.Link class.
10.11.2 Atom Support
Atom is another standard interchange format used to implement REST APIs. An example of Atom output can be seen below:
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="https://www.w3.org/2005/Atom">
<title>Example Feed</title>
<link href="https://example.org/"/>
<updated>2003-12-13T18:30:02Z</updated>
<author>
<name>John Doe</name>
</author>
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
<entry>
<title>Atom-Powered Robots Run Amok</title>
<link href="https://example.org/2003/12/13/atom03"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary>
</entry>
</feed>
To use Atom rendering again simply define a custom renderer:
import grails.rest.render.atom.*
beans = {
halBookRenderer(AtomRenderer, rest.test.Book)
halBookListRenderer(AtomCollectionRenderer, rest.test.Book)
}
10.11.3 Vnd.Error Support
Vnd.Error is a standardised way of expressing an error response.
By default when a validation error occurs when attempting to POST new resources then the errors object will be sent back allow with a 422 respond code:
$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d "" http://localhost:8080/books
HTTP/1.1 422 Unprocessable Entity
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=ISO-8859-1
{
"errors": [
{
"object": "rest.test.Book",
"field": "title",
"rejected-value": null,
"message": "Property [title] of class [class rest.test.Book] cannot be null"
}
]
}
If you wish to change the format to Vnd.Error then simply register grails.rest.render.errors.VndErrorJsonRenderer bean in grails-app/conf/spring/resources.groovy:
beans = {
vndJsonErrorRenderer(grails.rest.render.errors.VndErrorJsonRenderer)
// for Vnd.Error XML format
vndXmlErrorRenderer(grails.rest.render.errors.VndErrorXmlRenderer)
}
Then if you alter the client request to accept Vnd.Error you get an appropriate response:
$ curl -i -H "Accept: application/vnd.error+json,application/json" -H "Content-Type: application/json" -X POST -d "" http://localhost:8080/books
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/vnd.error+json;charset=ISO-8859-1
[
{
"logref": "book.nullable,
"message": "Property [title] of class [class rest.test.Book] cannot be null",
"_links": {
"resource": {
"href": "http://localhost:8080/rest-test/books"
}
}
}
]
10.12 Customizing Binding of Resources
The framework provides a sophisticated but simple mechanism for binding REST requests to domain objects and command objects. One way to take advantage of this is to bind the request property in a controller the properties of a domain class. Given the following XML as the body of the request, the createBook action will create a new Book and assign "The Stand" to the title property and "Stephen King" to the authorName property.
<?xml version="1.0" encoding="UTF-8"?>
<book>
<title>The Stand</title>
<authorName>Stephen King</authorName>
</book>
class BookController {
def createBook() {
def book = new Book()
book.properties = request
// ...
}
}
Command objects will automatically be bound with the body of the request:
class BookController {
def createBook(BookCommand book) {
// ...
}
}
class BookCommand {
String title
String authorName
}
If the command object type is a domain class and the root element of the XML document contains an id attribute, the id value will be used to retrieve the corresponding persistent instance from the database and then the rest of the document will be bound to the instance. If no corresponding record is found in the database, the command object reference will be null.
<?xml version="1.0" encoding="UTF-8"?>
<book id="42">
<title>Walden</title>
<authorName>Henry David Thoreau</authorName>
</book>
class BookController {
def updateBook(Book book) {
// The book will have been retrieved from the database and updated
// by doing something like this:
//
// book == Book.get('42')
// if(book != null) {
// book.properties = request
// }
//
// the code above represents what the framework will
// have done. There is no need to write that code.
// ...
}
}
The data binding depends on an instance of the DataBindingSource interface created by an instance of the DataBindingSourceCreator interface. The specific implementation of DataBindingSourceCreator will be selected based on the contentType of the request. Several implementations are provided to handle common content types. The default implementations will be fine for most use cases. The following table lists the content types which are supported by the core framework and which DataBindingSourceCreator implementations are used for each. All of the implementation classes are in the org.grails.databinding.bindingsource package.
| Content Type(s) | Bean Name | DataBindingSourceCreator Impl. |
|---|---|---|
application/xml, text/xml |
xmlDataBindingSourceCreator |
XmlDataBindingSourceCreator |
application/json, text/json |
jsonDataBindingSourceCreator |
JsonDataBindingSourceCreator |
application/hal+json |
halJsonDataBindingSourceCreator |
HalJsonDataBindingSourceCreator |
application/hal+xml |
halXmlDataBindingSourceCreator |
HalXmlDataBindingSourceCreator |
In order to provide your own DataBindingSourceCreator for any of those content types, write a class which implements
DataBindingSourceCreator and register an instance of that class in the Spring application context. If you
are replacing one of the existing helpers, use the corresponding bean name from above. If you are providing a
helper for a content type other than those accounted for by the core framework, the bean name may be anything that
you like but you should take care not to conflict with one of the bean names above.
The DataBindingSourceCreator interface defines just 2 methods:
package org.grails.databinding.bindingsource
import grails.web.mime.MimeType
import grails.databinding.DataBindingSource
/**
* A factory for DataBindingSource instances
*
* @since 2.3
* @see DataBindingSourceRegistry
* @see DataBindingSource
*
*/
interface DataBindingSourceCreator {
/**
* `return All of the {`link MimeType} supported by this helper
*/
MimeType[] getMimeTypes()
/**
* Creates a DataBindingSource suitable for binding bindingSource to bindingTarget
*
* @param mimeType a mime type
* @param bindingTarget the target of the data binding
* @param bindingSource the value being bound
* @return a DataBindingSource
*/
DataBindingSource createDataBindingSource(MimeType mimeType, Object bindingTarget, Object bindingSource)
}
AbstractRequestBodyDataBindingSourceCreator
is an abstract class designed to be extended to simplify writing custom DataBindingSourceCreator classes. Classes which
extend AbstractRequestbodyDatabindingSourceCreator need to implement a method named createBindingSource
which accepts an InputStream as an argument and returns a DataBindingSource as well as implementing the getMimeTypes
method described in the DataBindingSourceCreator interface above. The InputStream argument to createBindingSource
provides access to the body of the request.
The code below shows a simple implementation.
package com.demo.myapp.databinding
import grails.web.mime.MimeType
import grails.databinding.DataBindingSource
import org...databinding.SimpleMapDataBindingSource
import org...databinding.bindingsource.AbstractRequestBodyDataBindingSourceCreator
/**
* A custom DataBindingSourceCreator capable of parsing key value pairs out of
* a request body containing a comma separated list of key:value pairs like:
*
* name:Herman,age:99,town:STL
*
*/
class MyCustomDataBindingSourceCreator extends AbstractRequestBodyDataBindingSourceCreator {
@Override
public MimeType[] getMimeTypes() {
[new MimeType('text/custom+demo+csv')] as MimeType[]
}
@Override
protected DataBindingSource createBindingSource(InputStream inputStream) {
def map = [:]
def reader = new InputStreamReader(inputStream)
// this is an obviously naive parser and is intended
// for demonstration purposes only.
reader.eachLine { line ->
def keyValuePairs = line.split(',')
keyValuePairs.each { keyValuePair ->
if(keyValuePair?.trim()) {
def keyValuePieces = keyValuePair.split(':')
def key = keyValuePieces[0].trim()
def value = keyValuePieces[1].trim()
map<<key>> = value
}
}
}
// create and return a DataBindingSource which contains the parsed data
new SimpleMapDataBindingSource(map)
}
}
An instance of MyCustomDataSourceCreator needs to be registered in the spring application context.
beans = {
myCustomCreator com.demo.myapp.databinding.MyCustomDataBindingSourceCreator
// ...
}
With that in place the framework will use the myCustomCreator bean any time a DataBindingSourceCreator is needed
to deal with a request which has a contentType of "text/custom+demo+csv".
10.13 RSS and Atom
No direct support is provided for RSS or Atom within Grails. You could construct RSS or ATOM feeds with the render method’s XML capability.