在开发grails项目的时候用到了JSON数据的传递解析。但是使用render ... as JSON 返回的json数据会带转义字符,无奈替换转义字符数据还是不能用,只好看官方文档,于是看到了Grails Views的JSON Views.
这是一个插件,安装配置官方文档都说的很明白,但唯独怎么使用说的不是很清楚。
安装插件后,文档介绍Json数据在 文件后缀为.gson文件中处理。同时可以使用模板来配置复杂的json数据。一切都就绪后,但是不知道怎么调用。研究了一上午,终于搞定了。
比如说我想获得一个user的json数据。那么
retrun ["users":users,"userCount":count]
下面是文档介绍。很简单,也很好看明白。
JSON views are written in Groovy, end with the file extension gson
and reside in the grails-app/views
directory. They provide a DSL for producing output in the JSON format. A hello world example can be seen below:
grails-app/views/hello.gson
json.message {
hello "world"
}
The above JSON view results in the output:
{"message":{ "hello":"world"}}
The json
variable is an instance of StreamingJsonBuilder. See the documentation in the Groovy user guide for more information on StreamingJsonBuilder
.
More 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}]'
You can specify a model to view in one of two ways. Either with a model
block:
grails-app/views/hello.gson
model {
String message
}
json.message {
hello message
}
Or with the @Field
transformation provided by Groovy:
grails-app/views/hello.gson
import groovy.transform.Field
@Field String message
json.message {
hello message
}
The current release is 2.0.0.RC2. Below is a version history.
1.3.0
The includes
support has changed in a way that may break existing use cases. Having includes: ['captain', 'captain.name']]
previously excluded all other fields except for captain.name
. The captain
in the list was necessary for captain.name
to function as expected. Now the captain
entry will cause all fields in captain
to be rendered. To restore the previous behavior, simply remove the captain
entry. includes: ['captain', 'captain.name']
→ includes: ['captain.name]
.
1.2.0
JSON API support
Ability to register custom converters
New configuration options for date formatting and unicode escaping
New testing trait that integrates with the testing-support framework (1.2.5)
1.1.0
Global template support
Template inheritance
Global templates for GORM for MongoDB / GeoJSON
Support for easily testing JSON views with JsonViewTest
Pagination support in HAL
Better Embedded support in HAL
Ability to access HTTP parameters and headers
Resource expansion with the expand
parameter
Controller namespace support
Support for a default /object/_object.gson
template
1.0.0
Initial 1.0.0 GA Release
To activate JSON views, add the following dependency to the dependencies
block of your build.gradle
:
compile "org.grails.plugins:views-json:2.0.0.RC2"
If you are also using MongoDB you may want to add the views-json-templates
dependency too which includes support for GeoJSON:
compile "org.grails.plugins:views-json-templates:2.0.0.RC2"
To enable Gradle compilation of JSON views for production environment add the following to the buildscript
dependencies
block:
buildscript {
...
dependencies {
...
classpath "org.grails.plugins:views-gradle:2.0.0.RC2"
}
}
Then apply the org.grails.plugins.views-json
Gradle plugin after any Grails core gradle plugins:
...
apply plugin: "org.grails.grails-web"
apply plugin: "org.grails.plugins.views-json"
This will add a compileGsonViews
task to Gradle that is executed when producing a JAR or WAR file.
2.4.1. Template Basics
You can define templates starting with underscore _
. For example given the following template called _person.gson
:
grails-app/views/person/_person.gson
model {
Person person
}
json {
name person.name
age person.age
}
You can render the template with the g.render
method:
grails-app/views/person/show.gson
model {
Person person
}
json g.render(template:"person", model:[person:person])
The above assumes the view is in the same directory as the template. If this is not the case you may need to use a relative URI to the template:
grails-app/views/family/show.gson
model {
Person person
}
json g.render(template:"/person/person", model:[person:person])
2.4.2. Template Namespace
The previous example can be simplified using the template namespace:
grails-app/views/person/show.gson
model {
Person person
}
json tmpl.person(person)
In this example, the name of the method call (person
in this case) is used to dictate which template to render.
The argument to the template becomes the model. The name of the model variable is the same as the template name. If you wish to alter this you can pass a map instead:
grails-app/views/person/show.gson
model {
Person person
}
json tmpl.person(individual:person)
In the above example the model variable passed to the _person.gson
template is called individual
.
This technique may also be used when you want to render a template using a relative path:
grails-app/views/person/show.gson
model {
Person person
}
json tmpl.'/person/person'(person:person)
The template namespace even accepts a collection (or any Iterable
object):
grails-app/views/person/show.gson
model {
List people = []
}
json tmpl.person(people)
In this case the output is a JSON array. For example:
[{"name":"Fred",age:10},{"name":"Bob",age:12}]
When rendering an Iterable
, you can also specify the model name:
grails-app/views/person/show.gson
model {
List people = []
}
json tmpl.person("owner", people)
The person
template would have a field defined: Person owner
If you need additional data that will be static over each iteration of the template, you can also pass in a model:
grails-app/views/person/show.gson
model {
List people = []
}
json tmpl.person(people, [relationship: "Father"])
The person
template could have a field defined: String relationship
By passing in a collection the plugin will iterate over each element on the collection and render the template as a JSON array. If you do not want this to happen then use the variation of the method that takes a map instead:
grails-app/views/person/show.gson
model {
List people = []
}
json tmpl.person(people:people)
2.4.3. More Ways to Render Templates
The g.render
method is flexible, you can render templates in many forms:
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')
}
However, most of these use cases are more concise with the template namespace:
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 )
}
2.4.4. Template Inheritance
JSON templates can inherit from a parent template. For example consider the following parent template:
grails-app/views/_parent.gson
model {
Object object
}
json {
hal.links(object)
version "1.0"
}
A child template can inherit from the above template by using the inherits
method:
grails-app/views/_person.gson
inherits template:"parent"
model {
Person person
}
json {
name person.name
}
The JSON from the parent and the child template will be combined so that the output is:
{
"_links": {
"self": {
"href": "http://localhost:8080/person/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"version": "1.0",
"name": "Fred"
}
The parent template’s model will be formulated from the child templates model and the super class name. For example if the model is Person person
where Person
extends from Object
then the final model passed to the parent template will look like:
[person:person, object:person]
If the Person
class extended from a class called Mammal
then the model passed to the parent would be:
[person:person, mammal:person]
This allows you to design your templates around object inheritance.
You can customize the model passed to the parent template using the model
argument:
inherits template:"parent", model:[person:person]
2.5.1. Basics of Domain Class Rendering
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 provide additional JSON output:
json g.render(book) {
pages 1000
}
Or combine the two approaches:
json g.render(book, [includes:['title']]) {
pages 1000
}
2.5.2. Deep Rendering of Domain Classes
Typically the g.render(..)
method will only render objects one level deep. In other words if you have a domain class such as:
class Book {
String title
Author author
}
The resulting output will be something like:
{"id":1,"title":"The Stand","author":{id:1}}
If you wish for the author
to be included as part of the rendering, there are two requirements, first you must make sure the association is initialized.
If the render
method encounters a proxy, it will not traverse into the relationship to avoid N+1 query performance problems.
The same applies to one-to-many collection associations. If the association has not been initialized the render method will not traverse through the collection! |
So you must make sure your query uses a join:
Book.findByTitle("The Stand", [fetch:[author:"join"]])
Secondly when calling the render
method you should pass the deep
argument:
json g.render(book, [deep:true])
Alternatively, to only expand a single association you can use the expand
argument:
json g.render(book, [expand:['author']])
request parameters can also be used to expand associations (eg. ?expand=author ), if you do not want to allow this, then use includes or excludes to include only the properties you want. |
Finally, if you prefer to handle the rendering yourself you can do by excluding the property:
json g.render(book, [excludes:['author']]) {
author {
name book.author.name
}
}
2.5.3. Domain Class Rendering and Templates
An alternative to the default behaviour of the render
method is to rely on templates.
In other words if you create a /author/_author.gson
template and then use the g.render
method on an instance of book
:
json g.render(book)
Whenever the author
association is encountered the g.render
method will automatically render the /author/_author.gson
template instead.
All JSON views subclass the JsonViewTemplate class by default.
The JsonViewTemplate
superclass implements the JsonView trait which in turn extends the the GrailsView trait.
Thanks to these traits several methods and properties are available to JSON views that can be used to accomplish different tasks.
2.6.1. Creating Links
Links can be generated using the g.link(..)
method:
json.person {
name "bob"
homepage g.link(controller:"person", id:"bob")
}
The g.link
method is similar to the equivalent tag in GSP and allows you to easily create links to other controllers.
2.6.2. Altering the Response Headers
To customize content types and headers use the response
object defined by the HttpView trait:
response.contentType "application/hal+json"
response.header "Token", "foo"
json.person {
name "bob"
}
The HttpView
trait defines a variety of methods for inspecting the request and altering the response.
The methods available are only a subset of the methods available via the HttpServletRequest and HttpServletResponse objects, this is by design as view logic should be limited and logic performed in the controller instead. |
2.6.3. Accessing the Request
Various aspects of the request can be accessed by the request
object defined by the HttpView trait:
json.person {
name "bob"
userAgent request.getHeader('User-Agent')
}
Parameters can be accessed via the params
object which is an instance of Parameters:
json.person {
name "bob"
first params.int('offset', 0)
sort params.get('sort', 'name')
}
2.6.4. Default Static Imports
The following classes' static properties are imported by default:
org.springframework.http.HttpStatus
org.springframework.http.HttpMethod
grails.web.http.HttpHeaders
This means that you can use the response
object to set the status using these constants, instead of hard coded numbers:
response.status NOT_FOUND
Or generate links using the appropriate HTTP method:
g.link(resource:"books", method:POST)
2.6.5. I18n & Locale Integration
You can lookup i18n messages use the g.message
method:
json.error {
description g.message(code:'default.error.message')
}
You can also create locale specific views by appending the locale to view name. For example person_de.gson
for German or person.gson
for the default.
For more complex message lookups the messageSource property is an instance of the Spring MessageSource class. |
2.6.6. Accessing Configuration
The application configuration is injected automatically into your json views. To access it, simply reference config
.
json {
foo config.getProperty("bar", String, null)
}
2.6.7. Miscellaneous Properties
Other properties are also available:
|
The name of the current controller |
|
The name of the current controller action |
|
The namespace of the current controller |
Grails supports a convention for the model names in your JSON views. If the convention does not meet your needs, model variables can be explicitly defined.
Some model names are reserved since there are properties of the same name injected into the view:locale , response , request , page , controllerNamespace , controllerName , actionName , config , generator |
2.7.1. Explicit Model Naming
Given a view:
grails-app/views/hello/index.gson
model {
String message
}
json.message {
hello message
}
Then the controller has to specify the name to be used in the view:
grails-app/controllers/HelloController.groovy
def index() {
respond(message: "Hello World")
//or [message: "Hello World"]
}
When using a template:
grails-app/views/hello/_msg.gson
model {
String message
}
json.message {
hello message
}
In the view you can use the tmpl
namespace:
json {
message tmpl.msg([message: message])
// or g.render(template:'msg', model:[message: message])
// or g.render(template:'msg', model: message, var:'message')
}
Using collections:
model {
List messages
}
json {
messages tmpl.msg('message', messages)
// or g.render(template:'msg', collection: messages, var: 'message')
}
2.7.2. Model By Convention
Property Type
When rendering a non iterable object, the property name of the type is used when a name is not specified.
grails-app/views/hello/index.gson
model {
String string
}
json.message {
hello string
}
The variable can be passed in directly to respond
.
grails-app/controllers/HelloController.groovy
def index() {
respond("Hello World")
}
This also applies when rendering templates with tmpl
.
grails-app/views/hello/_msg.gson
model {
String string
}
json.message {
hello string
}
grails-app/views/hello/index.gson
model {
String someString
}
json tmpl.msg(someString)
If a collection is rendered, the property name of the component type is appended with the property name of the collection type. The component type is based on the first item in the list.
List
→ stringList
Set
→ stringSet
Bag
→ stringCollection
If the collection is empty, emptyCollection
will be used as the default model name. This is due to not being able to inspect the first object’s type.
grails-app/views/hello/index.gson
model {
String stringList
}
json {
strings stringList
}
The variable can be passed in directly to respond
.
grails-app/controllers/HelloController.groovy
def index() {
respond(["Hello", "World"])
}
The component+collection convention does not apply when rendering collections with tmpl inside a view. |
Template Name
When using a template, unless specified, the model name is based on the template name.
Given the following template:
grails-app/views/hello/_msg.gson
model {
String msg // or String string
}
json.message {
hello msg
}
To render a single message from another view using the template:
grails-app/views/hello/index.gson
json.message tmpl.msg(message)
To render a collection of messages from another view using the template:
grails-app/views/hello/index.gson
model {
List stringList
}
json {
messages tmpl.msg(stringList)
}
In both cases the convention of the variable name matching the template name is used.
GSON views integrate with Grails' content negotiation infrastructure. For example if you create two views called grails-app/views/book/show/show.gsp
(for HTML) and grails-app/views/book/show/show.gson
(for JSON), you can then define the following action:
grails-app/controllers/myapp/BookController.groovy
def show() {
respond Book.get(params.id)
}
The result is that if you send a request to /book/show
it will render show.gsp
but if you send a request to /book/show.json
it will render show.gson
.
In addition, if the client sends a request with the Accept
header containing application/json
the show.gson
view will be rendered.
2.8.1. Content Negotiation and Domain Classes
Content negotiation also works nicely with domain classes, for example if you want to define a template to render any instance of the Book
domain class you can create a gson
file that matches the class name.
For example given a class called demo.Book
you can create grails-app/views/book/_book.gson
and whenever respond
is called with an instance of Book
Grails will render _book.gson
.
def show() {
respond Book.get(params.id)
}
If you define an index
action that responds with a list of books:
def index() {
respond Book.list()
}
Then you can create a corresponding grails-app/views/book/index.gson
file that renders each book:
grails-app/views/book/index.gson
@Field List bookList
json tmpl.book(bookList)
When responding with a list of objects Grails automatically appends the suffix "List" to the model name, so in this case the model name is bookList |
By calling the tmpl.book(..)
method with the list of books the grails-app/views/book/_book.gson
template will be rendered for each one and a JSON array returned.
2.8.2. Global Default Template
You can also define a /object/_object
template that is rendered by default if no other template is found during content negotiation. To do this create a file called /grails-app/views/object/_object.gson
where the name of the model is object
, for example:
model {
Object object
}
json g.render(object)
2.8.3. Content Negotiation and Versioned APIs
A typical use case when building REST APIs is the requirement to support different versions of the API. GSON views can be versioned by including the version in the name of the view.
Grails will then use the ACCEPT-VERSION
header when resolving the view.
For example given a view called /book/show.gson
if you wish to deprecate your previous API and create a version 2.0 API, you can rename the previous view /book/show_v1.0.gson
and create a new /book/show.gson
representing the new version of the API.
Then when the client sends a request with the ACCEPT-VERSION
header containing v1.0
the /book/show_v1.0.gson
view will be rendered instead of /book/show.gson
.
2.8.4. Content Negotiation and View Resolving Strategy
Grails takes into account a number of factors when attempting to resolve the view including the content type, version and locale.
The following paths are searched:
view_name[_LOCALE][_ACCEPT_CONTENT_TYPE][_ACCEPT-VERSION].gson (Example: show_de_hal_v1.0.gson)
view_name[_LOCALE][_ACCEPT_CONTENT_TYPE].gson (Example: show_de_hal.gson)
view_name[_LOCALE][_ACCEPT-VERSION].gson (Example: show_de_v1.0.gson)
view_name[_LOCALE].gson (Example: show_de.gson)
view_name[_ACCEPT_CONTENT_TYPE][_ACCEPT-VERSION].gson (Example: show_hal_v1.0.gson)
view_name[_ACCEPT-VERSION][_ACCEPT_CONTENT_TYPE].gson (Example: show_v1.0_hal.gson)
view_name[_ACCEPT_CONTENT_TYPE].gson (Example: show_hal.gson)
view_name[_ACCEPT-VERSION].gson (Example: show_v1.0.gson)
view_name.gson (Example: show.gson)
The content type (defined by either the ACCEPT
header or file extension in the URI) is taken into account to allow different formats for the same view.
2.8.5. Content Negotiation and Custom Mime Types
Some REST APIs use the notion of custom mime types to represent resources. Within Grails you can for example define custom mime types in grails-app/conf/application.yml
:
grails:
mime:
types:
all: "*/*"
book: "application/vnd.books.org.book+json"
bookList: "application/vnd.books.org.booklist+json"
Once these custom mime types have been defined you can then define a view such as show.book.gson
for that particular mime type.
HAL is a standard format for representing JSON that has gained traction for its ability to represent links between resources and provide navigable APIs.
The JSON views plugin for Grails provides HAL support out-of-the-box. All JSON views have access to the hal
instance which implements HalViewHelper.
For example:
model {
Book book
}
json {
hal.links(book)
hal.embedded {
author( book.authors.first() ) { Author author ->
name author.name
}
}
title book.title
}
The call to hal.links() has to be the first element within the json{} closure. |
This produces the HAL output:
{
"_links": {
"self": {
"href": "http://localhost:8080/book/show/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"author": {
"_links": {
"self": {
"href": "http://localhost:8080/author/show/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Stephen King"
}
},
"title": "The Stand"
}
2.9.1. Generating HAL Links
The above example uses the hal.links(..)
method to render links for a domain resource and the hal.embedded(..)
method to define any embedded objects that form part of the HAL response.
The hal.links(..)
method will by default create a link to the resource, but you can define additional links by annotating the domain class with either grails.rest.Linkable
or grails.rest.Resource
and using the link
method on the object:
book.link(rel:"publisher", href:"http://foo.com/books")
The link will then be included in the HAL output.
If you wish to be specific about which links to include you can do so by passing a map of objects to link to:
model {
Book book
}
json {
hal.links(self:book, author: book.author)
...
}
Alternatively, if you prefer to define the HAL links yourself then you can do so by passing a closure to the hal.links
method:
model {
Book book
}
json {
hal.links {
self {
href '...'
hreflang 'en'
type "application/hal+json"
}
}
...
}
2.9.2. Rendering Domain Classes as HAL
If you prefer to let the plugin handle the rendering of your object you can use the hal.render(..)
method:
model {
Book book
}
json hal.render(book)
The hal.render
method works the same as the g.render
method, accepting the same arguments, the difference being it will output HAL links for the object via hal.links
and also output associations fetched via a join query for hal.embedded
.
For example you can also customize the content of the generated HAL with a closure:
model {
Book book
}
json hal.render(book) {
pages 1000
}
2.9.3. Embedded Association and HAL
Generally, when using the hal.render(..)
method, _embedded
associations are only rendered for associations that have been initialized and eagerly fetched. This means that the following query will not render the book.authors
association:
Book.get(params.id)
However, this query will render the book.authors
association:
Book.get(params.id, [fetch:[authors:'eager']])
This is by design and to prevent unexpected performance degradation due to N+1 query loading. If you wish to force the render
method to render _embedded
associations for HAL you can do see using the deep
argument:
json hal.render(book, [deep:true])
You can prevent HAL _embedded
associations from being rendering using the embedded:false
parameter:
model {
Book book
}
json hal.render(book, [embedded:false])
You can also render embedded associations without using the hal.render(..)
method, by using the hal.embedded(..)
method:
model {
Book book
}
json {
hal.embedded(book)
title book.title
}
Like the hal.links(..) method, the hal.embedded(..) method should come first, before any other attributes, in your JSON output |
You can also control which associations should be embedded by using a map argument instead:
model {
Book book
}
json {
hal.embedded(authors: book.authors)
title book.title
}
And you can inline the contents of the book without any associations using the hal.inline(..)
method:
model {
Book book
}
json {
hal.embedded(authors: book.authors)
hal.inline(book)
}
To customize the contents of the inlined JSON output use a closure:
model {
Book book
}
json {
hal.embedded(authors: book.authors)
hal.inline(book) {
pages 300
}
}
You cannot include additional content after the call to hal.inline(..) as this will produce invalid JSON |
You can combine hal.embeddded(..)
and hal.links(..)
to obtain exactly the links and the embedded content you want:
model {
Book book
}
json {
hal.links(self: book )
hal.embedded(authors: book.authors)
hal.inline(book) {
pages 300
}
}
2.9.4. Specifying the HAL Content Type
The default HAL response content type is application/hal+json
, however as discussed in the section on Custom Mime Type you can define your own response content types to represent your resources.
For example given the following configuration in grails-app/conf/application.yml
:
grails:
mime:
types:
all: "*/*"
book: "application/vnd.books.org.book+json"
You can set the HAL content type to an explicit content type or one of the named content types defined in grails.mime.types
in application.yml
:
model {
Book book
}
hal.type("book")
json {
...
}
2.9.5. HAL Pagination
The JSON views plugin for Grails provides navigable pagination support. Like the GSP
tag, the parameters include: total
, max
, offset
, sort
and order
.
For example:
model {
Iterable bookList
Integer bookCount
Integer max // optional, defaults to 10
Integer offset // optional, defaults to 0
String sort // optional
String order // optional
}
json {
hal.paginate(Book, bookCount, max, offset, sort, order)
...
}
Similar to hal.links() the hal.paginate() has to be the first element within the json{} closure. |
When accessing http://localhost:8080/book?offset=10
this produces the navigable output like:
{
"_links": {
"self": {
"href": "http://localhost:8080/book/index?offset=10&max=10",
"hreflang": "en_US",
"type": "application/hal+json"
},
"first": {
"href": "http://localhost:8080/book/index?offset=0&max=10",
"hreflang": "en_US"
},
"prev": {
"href": "http://localhost:8080/book/index?offset=0&max=10",
"hreflang": "en_US"
},
"next": {
"href": "http://localhost:8080/book/index?offset=20&max=10",
"hreflang": "en_US"
},
"last": {
"href": "http://localhost:8080/book/index?offset=40&max=10",
"hreflang": "en_US"
}
},
...
}
If there aren’t enough results to paginate the navigation links will not appear. Likewise the prev and next links are only present when there is a previous or next page. |
If you have other links that you want to include along with the pagination links then you can use the hal.links(..)
method with pagination arguments:
model {
Author author
Iterable bookList
Integer bookCount
}
json {
// the model, type to paginate, and the total count
hal.links([author:author], Book, bookCount)
...
}
JSON API is a standard format for representing JSON.
The JSON views plugin for Grails provides JSON API support out-of-the-box. All JSON views have access to the jsonapi
instance which implements JsonApiViewHelper.
Grails views makes a best effort to conform to the JSON API standard, however that only applies to the format of the data. The specification makes many assertions about response codes, url conventions, and other server related concepts that are overreaching.
The JSON API support in Grails also only applies to domain classes. Because the specification relies on concepts of relationships and identifiers, there is no clear way to determine how those concepts would apply to conventional Groovy classes. As a result, normal Groovy classes are not supported.
The simplest example of using JSON API simply passes a domain class to the render
method of the jsonapi
object.
For example:
model {
Book book
}
json jsonapi.render(book)
In the example where Book
looks like the following:
class Book {
String title
Author author
}
This is what an example Book
instance rendered with JSON API might look like.
{
"data": {
"type": "book",
"id": "3",
"attributes": {
"title": "The Return of the King"
},
"relationships": {
"author": {
"links": {
"self": "/author/9"
},
"data": {
"type": "author",
"id": "9"
}
}
}
},
"links": {
"self": "http://localhost:8080/book/3"
}
}
2.10.1. JSON API Object
To include the JSON API Object in the response, include a jsonApiObject
argument to the render
method.
model {
Book book
}
json jsonapi.render(book, [jsonApiObject: true])
The response will contain "jsonapi":{"version":"1.0"}
as the first key in the resulting object.
2.10.2. Meta Object
To add metadata to your response, the meta
argument can be passed to render with the value being the object that should be rendered. If this functionality is used in addition to jsonApiObject: true
, the metadata will be rendered as the "meta" property of the "jsonapi" object.
model {
Book book
}
json jsonapi.render(book, [meta: [name: "Metadata Information"]])
The response will contain "meta":{"name":"Metadata Information"}
as the first key in the resulting object.
2.10.3. Rendering Errors
The JSON API specification has instructions on how to render errors. In the case of the Grails implementation, this applies to both exceptions and validation errors.
Exceptions
If an exception is passed to the render
method, it will be rendered within the specification.
In the example of an exception new RuntimeException("oops!")
, the following will be rendered:
{
"errors": [
{
"status": 500,
"title": "java.lang.RuntimeException",
"detail": "oops!",
"source": {
"stacktrace": [
//An array of information relating to the stacktrace
]
}
}
]
}
Validation Errors
In the case of validation errors, the response will look like the following when a name
property fails the blank constraint:
{
"errors": [
{
"code": "blank",
"detail": "Property [name] of class [class com.foo.Bar] cannot be blank",
"source": {
"object": "com.foo.Bar",
"field": "name",
"rejectedValue": "",
"bindingError": false
}
}
]
}
2.10.4. Links
In general, links for relationships will be provided when the relationship has a value.
Example output for a has one relationship where the value is null:
"captain": {
"data": null
}
And when the value is not null:
"author": {
"links": {
"self": "/author/9"
},
"data": {
"type": "author",
"id": "9"
}
}
Currently links are not supported in the case of to many relationships.
2.10.5. Pagination
The JSON API specification has a section which explains pagination. The Grails implementation follows that specification as it is written.
To enable pagination links in your output you must be rendering a collection and also include some arguments in addition to the collection being rendered. There are two required arguments: total and resource. The default offset is 0 and max is 10.
model {
List books
Integer bookTotal
}
json jsonapi.render(books, [pagination: [total: bookTotal, resource: Book]])
Example links output if bookTotal == 20
:
"links": {
"self": "/books",
"first": "http://localhost:8080/books?offset=0&max=10",
"next": "http://localhost:8080/books?offset=10&max=10",
"last": "http://localhost:8080/books?offset=10&max=10"
}
By default the values for offset
, sort
, max
, and order
will come from the parameters with the same names. You can override their values by passing the corresponding argument in the pagination Map.
model {
List books
Integer bookTotal
}
json jsonapi.render(books, [pagination: [total: bookTotal, resource: Book, max: 20, sort: params.myCustomSortArgument]])
2.10.6. Associations
The JSON API specification details how relationships should be rendered. The first way is through a relationships object described here. By default that is now relationships will be rendered in json views.
If you do not wish to render the relationships at all, the associations
argument can be passed to render
with the value of false.
model {
Book book
}
json jsonapi.render(book, [associations: false])
The specification also has a section that describes compound documents. If you want one or more of your relationships to be rendered in that manner, you can include the expand
argument.
model {
Book book
}
json jsonapi.render(book, [expand: "author"]) //can also be a list of strings
2.10.7. Includes / Excludes
The JSON API implementation in Grails supports the same includes and excludes support as normal json views. Please see the section on rendering for details.
2.10.8. Identifier Rendering
Grails provides a way to customize the rendering of your domain class identifiers. To override the default behavior, register a bean that implements JsonApiIdRenderStrategy.
grails-app/conf/spring/resources.groovy
beans = {
jsonApiIdRenderStrategy(MyCustomImplementation)
}
The JSON Views plugin registers a bean called jsonTemplateEngine
of type JsonViewTemplateEngine.
This class is a regular Groovy TemplateEngine and you can use the engine to render JSON views outside the scope of an HTTP request.
For example:
@Autowired
JsonViewTemplateEngine templateEngine
void myMethod() {
Template t = templateEngine.resolveTemplate('/book/show')
def writable = t.make(book: new Book(title:"The Stand"))
def sw = new StringWriter()
writable.writeTo( sw )
...
}
JSON views are statically compiled. You can disable static compilation if you prefer by setting grails.views.json.compileStatic
:
grails:
views:
json:
compileStatic: false
If you disable static compilation rendering performance will suffer. |
For model variables you need to declare the types otherwise you will get a compilation error:
model {
Person person
}
json {
name person.name
age person.age
}
Although generally testing can be done using functional tests via an HTTP client, the JSON views plugin also provides a trait which helps in writing either unit or integration tests.
To use the trait import the grails.plugin.json.view.test.JsonViewTest
class and apply it to any Spock or JUnit test:
import grails.plugin.json.view.test.*
import spock.lang.Specification
class MySpec extends Specification implements JsonViewTest {
...
}
The trait provides a number of different render
method implementations that can either render a JSON view found in grails-app/views
or render an inline String. For example to render an inline template:
void "Test render a raw GSON view"() {
when:"A gson view is rendered"
JsonRenderResult result = render '''
model {
String person
}
json.person {
name person
}
''', [person:"bob"]
then:"The json is correct"
result.json.person.name == 'bob'
}
Use the render method to return a JsonRenderResult passing in a String that is the inline template and a map that is the model |
|
Assert the parsed JSON, represented by the json property, is correct |
To render an existing view or template use named arguments to specify an absolute path to either a template or a view:
when:"A gson view is rendered"
def result = render(template: "/path/to/template", model:[age:10])
then:"The json is correct"
result.json.name == 'Fred'
result.json.age == 10
If you are writing a unit test, and the model involves a domain class, you may need to add the domain class to the mappingContext
object in order for it to be rendered correctly:
when:"A gson view is rendered for a domain class"
mappingContext.addPersistentEntity(MyDomain)
def result = render(template: "/path/to/template", model:[myDomain:MyDomain])
then:"The json is correct"
result.json.name == 'Fred'
result.json.age == 10
Links generated by json views in a unit test may not match what they would normally generate in the standard environment. To fully test links, use a functional test. |
2.13.1. New Testing Framework
Since the release of json views, a new testing framework was released for Grails. A new trait has been developed that works with the new testing framework that is designed to replace the existing trait. The existing trait will be left as is for backwards compatibility.
The new trait works exactly the same way as the old one, however since the new trait is designed to work with the new framework, there are several benefits you can take advantage of. The first is configuration. In the old trait the application configuration was not included in the template engine, which had the potential to produce incorrect results. Other benefits include extensibility and features like @OnceBefore
.
To get started, add the org.grails:views-json-testing-support:{version}
dependency to your project and implement the JsonViewUnitTest trait in your test instead of JsonViewTest
.
The new testing trait, like the testing framework, requires Spock. |
Grails plugins as well as standard Gradle Java/Groovy projects can provide json views to your application.
2.14.1. Grails Plugins
Since JSON views are compiled all of a plugin’s views and templates are available for use in your applications.
The view resolution will look through all of the application’s configured plugins for views that match a particular name. By default, the views a plugin supplies should be stored in grails-app/views
, just like applications.
2.14.2. Basic Libraries
The most common use case to provide views in a standard library is to provide global templates. Global templates are templates designed to render a given class as JSON. In order to provide views in a standard Gradle project, you should configure your own view compilation task.
Below is an example Gradle build that adds a compileViews
task for templates located into src/main/gson
:
buildscript {
repositories {
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
classpath "org.grails.plugins:views-gradle:2.0.0.RC2"
}
}
import grails.views.gradle.json.*
apply plugin:"java"
repositories {
maven { url "https://repo.grails.org/grails/core" }
}
dependencies {
compile "org.grails.plugins:views-json:2.0.0.RC2"
compileOnly "org.grails:grails-plugin-rest:3.1.7"
compileOnly "javax.servlet:javax.servlet-api:4.0.1"
}
task( compileViews, type:JsonViewCompilerTask ) {
source = project.file('src/main/gson')
destinationDir = project.file('build/classes/main')
packageName = ""
classpath = configurations.compile + configurations.compileOnly
}
classes.dependsOn compileViews
Once this is in place any applications that includes this library will have access to the templates provided.
For example if you want to render all instances of type foo.bar.Birthday
create a template called src/main/gson/foo/bar/_birthday.gson
then compile the template and place the JAR on the classpath of your application.
See the GeoJSON templates for MongoDB for example of how it provides global templates for Mongo specific classes. |
2.14.3. Customizing the Compile Task
Unless otherwise configured, the project name of the plugin (the gradle project.name
) is used as the packageName
when compiling JSON views.
In Gradle project.name
is generally calculated from the current directory. That means that if there is mismatch between the current directory and your plugin name view resolution from a plugin could fail.
For example consider a plugin named FooBarGrailsPlugin
, in this case Grails will search for views that match the plugin name foo-bar
. However, if the directory where the plugin is located is called fooBar
instead of foo-bar
then view resolution will fail and the view will not be found when the plugin is installed into an application.
To resolve this issue you can customize the compileGsonViews
task in the plugin’s build.gradle
compileGsonViews {
packageName = "foo-bar"
}
By setting the packageName
property to correctly match the convention of the plugin named (FooBarGrailsPlugin
maps to foo-bar
) view resolution will succeed.
JSON views configuration can be altered within grails-app/conf/application.yml
. Any of the properties within the JsonViewConfiguration interface can be set. The json view configuration extends GenericViewConfiguration, therefore any properties in that interface can be set as well.
For example:
grails:
views:
json:
compileStatic: true
cache: true
...
Alternatively you can register a new JsonViewConfiguration
bean using the bean name jsonViewConfiguration
in grails-app/conf/resources.groovy
.
The same settings in grails-app/conf/application.yml
will also be used by the Gradle plugin for production compilation.
The Gradle plugin compiles views using a forked compiler. You can configure the forked compilation task in Gradle as follows:
compileGsonViews {
compileOptions.forkOptions.memoryMaximumSize = '512mb'
}
See the API for GroovyForkOptions for more information.
2.15.1. Changing the view base class
All JSON views subclass the JsonViewTemplate class by default.
You can however change the subclass (which should be a subclass of JsonViewTemplate
) using configuration:
grails:
views:
json:
compileStatic: true
baseTemplateClass: com.example.MyCustomJsonViewTemplate
2.15.2. Adding New Helper Methods via Traits
Alternatively, rather than modifying the base class, you can instead just add new methods via traits.
For example the HttpView uses the Enhances
annotation to add the page
object to all views:
import grails.artefact.Enhances
import grails.views.Views
@Enhances(Views.TYPE)
trait HttpView {
/**
* @return The response object
*/
Response response
...
}
The result is all JSON views have a response
object that can be used to control the HTTP response:
response.header "Token", "foo"
The trait cannot be defined in the same project as you are compilation as it needs to be on the classpath of the project you are compiling. You will need to create a Grails plugin and use a multi-project build in this scenario. |
2.15.3. Default Mime Types
The mime types that will be accepted by default for JSON view rendering is configurable.
grails:
views:
json:
mimeTypes:
- application/json
- application/hal+json
2.15.4. Unicode Escaping
By default, unicode characters will not be escaped. If you would like unicode characters to be escaped in your output, set the following configuration.
grails:
views:
json:
generator:
escapeUnicode: true
2.15.5. Default Date Format
The default format for java.util.Date
can be configured.
grails:
views:
json:
generator:
dateFormat: "yyyy-MM-dd'T'HH:mm:ss'Z'"
In addition, the default locale can also be specified.
grails:
views:
json:
generator:
locale: "en/US"
2.15.6. Default Time Zone
The default time zone can be configured. The value here will be passed to TimeZone.getTimeZone(...)
grails:
views:
json:
generator:
timeZone: "GMT"
It is possible to register custom converters to change how a given class is rendered with json views. To do so, create a class that implements Converter. Then you must register the class in src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter
.
package foo
class MyConverter implements JsonGenerator.Converter {
@Override
boolean handles(Class> type) {
CustomClass.isAssignableFrom(type)
}
@Override
Object convert(Object value, String key) {
((CustomClass)value).name
}
}
class CustomClass {
String name
}
Because plugins could potentially provide converters, you can also determine the order by implementing the Orderedinterface in your converter.
src/main/resources/META-INF/services/grails.plugin.json.builder.JsonGenerator$Converter
foo.MyCustomConverter
If you have multiple classes to register, put each one on it’s own line |
When opening .gson
files in Intellij they should be opened with the regular Groovy editor.
The plugin includes an IntelliJ GDSL file that provides code completion when using IntelliJ IDEA.
Intellij GDSL is a way to describe the available methods and properties so that code completion is available to the developer when opening .gson
files.
The GDSL file is generally kept up-to-date with the codebase, however if any variation is spotted please raise an issue.
Generally views should be kept simple and if you arrive to the point where you need to debug a view you probably have too much logic in the view that would be better off handled by the controller prior to supplying the model.
Nevertheless here are a few view debugging tips to help identify problems in the view.
2.18.1. Introspecting the Model
Every JSON view is a Groovy script and like any Groovy script the model is defined in a binding
variable. Therefore you can easily find out the model at any time by either logging or printing the binding variables:
model {
Book book
}
// use the log variable
log.debug "Model is $binding.variables"
// use system out
System.out.println "Model is $binding.variables"
json g.render(book)
If you using the log variable then you will need to enabling logging for the grails.views package in grails-app/conf/logback.groovy |
2.18.2. Connecting a Debugger
Some IDEs like Intellij IDE allow you to set break points and step debug within the view itself. As mentioned previously you shouldn’t arrive to this point and if you do you have too much logic in your view, but if you do need it the feature is there.