Springfox 3: migrate from ResponseMessage to Response to override error messages - springfox

Since Springfox 3, ResponseMessage is deprecated and we should use Response instead.
How can I properly migrate from ResponseMessage to Response to override default response error messages?
This is an example of springfox 2.9.2 compatible code that I want to migrate:
private static ResponseMessage responseMessageFor401() {
return new ResponseMessageBuilder()
.code( 401 )
.message( "ERROR_MESSAGE_401" )
.responseModel( new ModelRef( "StatusWrapper" ) )
.build();
}

I finally found the answer:
You must add your model class to Docket configuration.
.additionalModels( typeResolver.resolve( StatusWrapper.class ) )
And than:
private static Response responseMessageFor401() {
return new ResponseBuilder().code( "401" )
.description( "ERROR_MESSAGE_401" )
.representation( MediaType.APPLICATION_JSON )
.apply( DefaultResponse::buildDefaultModel )
.isDefault( true )
.build();
}
private static void buildDefaultModel( final RepresentationBuilder representationBuilder ) {
representationBuilder.model(
msBuilder ->
msBuilder.name( "StatusWrapper" )
.referenceModel( rmsBuilder ->
rmsBuilder.key(
mkBuilder ->
mkBuilder.isResponse( true )
.qualifiedModelName(
qmnBuilder -> qmnBuilder.name( "StatusWrapper" )
//package of your model class
.namespace( "com.example.swagger.configuration" )
.build() )
.build() )
.build() )
.build() );
}

Related

How can I create a generic implementation of fromJson and toJson in Dart?

I am attempting to implement an inheritance pattern that allows for creating instances of an extended class using generic types. The below code appears to be a working pattern, yet the compiler complains that T.fromJson() is not implemented. Specifically, it gets red underlined squiggles in VS Code with the following error:
The method 'fromJson' isn't defined for the type 'Type'. Try correcting the name to the name of an existing method, or defining a method named 'fromJson'.
I am encountering this during a refactor effort but was able to create a semi-simplified example of the pattern I think should work:
class TestClass{
TestClass();
factory TestClass.fromJson( Map<String, dynamic> json ) => TestClass();
}
class ATest extends TestClass {
ATest();
#override
factory ATest.fromJson( Map<String, dynamic> json ) => ATest();
}
class BTest extends TestClass {
BTest();
#override
factory BTest.fromJson( Map<String, dynamic> json ) => BTest();
}
class CTest extends TestClass {
CTest();
#override
factory CTest.fromJson( Map<String, dynamic> json ) => CTest();
}
class FactoryController<T extends TestClass>{
Future<List<T>> listNetworkObjects<T extends TestClass>({
required List<String> filters,
}) async {
try {
final data = await MockApi().mockApiCall<T>( filters );
final List<T> allItems = List<T>.from(
data.map( ( T model ) => T.fromJson( model ) )
);
if ( allItems.isNotEmpty ) {
print( 'Received Items from the API.' );
}
return allItems;
}
catch ( getTestClassError, stackTrace ) {
print( 'Error getting TestClasses' );
print( stackTrace );
return <T>[];
}
}
}
class MockApi{
MockApi();
dynamic mockApiCall<T extends TestClass>(
List<String> filters
) async {
final dynamic data = { "test": "object" };
return data;
}
}
I would expect that the method listNetworkObjects would provide visibility to the compiler that the base class and all implementations of it also provide an implementation of the .fromJson() method of the class.
Instead of data.map( ( T model ) => T.fromJson( model ) ) I have tried:
data.map( ( T model ) => T().fromJson( model ) )
data.map( ( T model ) => new T.fromJson( model ) )
data.map( ( T model ) => new T().fromJson( model ) )
data.map( ( model as T ) => T.fromJson( model ) )
data.map( ( T model ) => T.new.fromJson( model ) )
each of which fails for a different reason.
I see other similar questions about abstract class constructors etc. like
Flutter abstract class with factory methods
but my base class is not abstract. I also tried with using implements instead of extends but:
listNetworkObjects<T implements TestClass>()
is not a valid method or class specification. I feel like there could be an answer here but I can't figure out how to implement it properly:
Creating an instance of a generic type in DART.
Thanks!
It is a limitation of Dart that static methods of generic types cannot be called (including constructors and factories).
In some situations I've used a pattern like below, with the disadvantage that it requires an instantiated object of the type to be passed in:
abstract class TestClass{
TestClass fromJson(Map<String, dynamic> json);
}
class Foo<T extends TestClass> {
T someMethod(T proto) => proto.fromJson(...) as T;
}

Pass information to new web-browser window/tab being opened with BrowserWindowOpener in Vaadin 8

In Vaadin 8, we can let the user open a new tab/window within their web browser by clicking a button that has been associated with a BrowserWindowOpener. As discussed in the manual, a new UI is instantiated on our behalf. All we pass is a .class object, the class of our UI subclass to be instantiated for display in the new window/tab. For example:
BrowserWindowOpener opener = new BrowserWindowOpener( PersonDetailUI.class );
That works for me. My question is: How do I pass some information to that new UI object in that new window/tab?
For example, I might need to pass:
The ID number or UUID of a record to be looked up in a database.
A JavaBean object ready for display in a layout.
I see I asked about this same issue for Vaadin 7. Consider this an updated version of the Question for Vaadin 8. The only Answer there speculated about adding parameters to the URI of the new window. But that limits me to a small piece of text. I prefer to pass a smart object rather than a dumb string.
There are basically three approaches you can use, and combinations of these
Use URI parameters. As you mentioned, this is limited to String type data.
You can read the URI fragment in UI with e.g. String uriFragment = Page.getCurrent().getUriFragment();
You can read URI parameter using VaadinRequest.getParameter(), VaadinRequest is given as parameter in init(...) of main UI
UI's in different browser tabs share the same Vaadin session. That gives some tools, namely
You can use session attributes, i.e. VaadinSession.getCurrent().getAttribute(…) and VaadinSession.getCurrent().setAttribute(…)
If you use CDI or Spring, you can Inject / Autowire #VaadinSessionScoped bean. The instance is then bound to Session and hence shared between the tabs.
Read data from database (possibly using 1. and/or 2. as help for keys)
The Answer by Tatu Lund mentioned passing a URI parameter to the new window being opened.
Example app, passing URI parameter to new window
Here is a simple little demonstration app in Vaadin 8.5.0 to show that technique.
We start with three cats that we pretend to retrieve from a database. Each time the user selects a cat, we get the cat’s identifier, a UUID object. We generate a canonical 32-character hex string representation of that UUID. And we specify that as the parameter value to be passed with the parameter key cat_id. We specify that parameter by calling BrowserWindowOpener::setParameter.
Note that we do this on the selection of an item in the Grid listing cats. Because of browser restrictions in opening windows, the BrowserWindowOpener must be configured before the user clicks its button. We cannot run code in reaction to the button click, as far as I know.
The new browser window is populated with an automatically-instantiated subclass of UI. In this case CatUI is what we wrote. In CatUI, we get the URI parameter, extract the string representing the UUID, recreate the UUID object, and then pass that to our pretend database-service to fetch the cat in question. Then a layout’s fields are populated with the Cat object values. We don’t bother with data-binding the cat-to-layout as that is not the point of this demo.
Caveat: This is just a demo, and ignores issues that are not directly related to issue of passing information via URI parameter to a new window. For example, crucial issues of concurrency are ignored here but would not be in real work.
This demo consists of four class files, all pasted below.
MainUI (opened by default when app launches)
CatUI (opened when user clicks button)
Cat (business object, with name and id properties)
DatabaseService (pretend storehouse of cat records)
MainUI
package com.basilbourque.example;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.server.BrowserWindowOpener;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.Button;
import com.vaadin.ui.Grid;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import javax.servlet.annotation.WebServlet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
* This UI is the application entry point. A UI may either represent a browser window
* (or tab) or some part of an HTML page where a Vaadin application is embedded.
* <p>
* The UI is initialized using {#link #init(VaadinRequest)}. This method is intended to be
* overridden to add component to the user interface and initialize non-component functionality.
*/
#Theme ( "mytheme" )
public class MainUI extends UI {
private Grid< Cat > grid;
private Button button;
#Override
protected void init ( VaadinRequest vaadinRequest ) {
// Configure button to open now browser window/tab.
this.button = new Button( "Open in new window" );
BrowserWindowOpener opener = new BrowserWindowOpener( CatUI.class );
opener.setFeatures( "height=300,width=500,resizable" );
opener.extend( this.button );
opener.setParameter( "cat_id" , new UUID(0,0).toString()); // Send nil UUID (all zeros) if nothing selected.
System.out.println( "BWO URL: " + opener.getUrl() );
this.button.setEnabled( false );
this.grid = new Grid<>( Cat.class );
this.grid.setCaption( "Cats" );
List<Cat> cats = new DatabaseService().fetchAllCats() ;
this.grid.setItems( cats );
// Every time the user selects a cat in the Grid, assign that cat’s ID to our `BrowserWindowOpener`. This way our button is always prepared to open a window for the selected cat.
this.grid.addSelectionListener( event -> {
Set< Cat > selectedCats = event.getAllSelectedItems();
this.button.setEnabled( selectedCats.size() > 0 );
if ( selectedCats.size() > 0 ) { // If the user selected an item.
Cat cat = selectedCats.stream().findFirst().get();
opener.setParameter( "cat_id" , cat.getId().toString() ); // A UUID’s canonical presentation is as a 36-character hexadecimal string in five groups with HYPHEN-MINUS as delimiter.
} else {
opener.setParameter( "cat_id" , new UUID(0,0).toString()); // Send nil UUID (all zeros) if nothing selected.
}
System.out.println( "BWO URL: " + opener.getUrl() );
} );
this.grid.select( cats.stream().findFirst().get() ); // Select first item arbitrarily, to provoke the grid’s selection-listener above to fire.
button.addClickListener( e -> {
System.out.println( "BASIL opening now window" );
} );
final VerticalLayout layout = new VerticalLayout();
layout.addComponents( this.grid , button );
setContent( layout );
}
#WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
#VaadinServletConfiguration ( ui = MainUI.class, productionMode = false )
public static class MyUIServlet extends VaadinServlet {
}
}
CatUI
package com.basilbourque.example;
import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.*;
import java.util.Optional;
import java.util.UUID;
public class CatUI extends UI {
private Cat cat = null;
#Override
protected void init ( VaadinRequest vaadinRequest ) {
// Retrieve a parameter from the URI of this UI/window.
String catUuidString = vaadinRequest.getParameter( "cat_id" ); // In the URI key-value parameters, "cat_id" is our key, and a UUID’s hex string is the expected value.
if ( null == catUuidString ) { // If we did not receive the UUID-string parameter we expected.
this.setContent( this.buildLayoutForNoCat( null ) );
return;
}
UUID uuid = UUID.fromString( catUuidString ); // Rehydrate the `UUID` from our passed hex string representing the UUID’s value.
Optional< Cat > cat = new DatabaseService().fetchCat( uuid );
if ( cat.isPresent() ) { // NULL check.
System.out.println( "uuidString: " + uuid + " and cat: " + cat.get() );
this.setContent( this.buildLayoutForCat( cat.get() ) ); // Retrieve the `Cat` object from our `Optional< Cat >` object by calling `get()` only after checking for NULL.
return;
} else { // Failed to find cat.
this.setContent( this.buildLayoutForNoCat( uuid ) );
return;
}
}
private Layout buildLayoutForCat ( Cat cat ) {
this.cat = cat ;
this.getPage().setTitle( "Cat details" );
// Have some content for it
TextField name = new TextField( "Name: " );
name.setWidth( 100 , Unit.PERCENTAGE );
name.setValue( this.cat.getName() );
TextField id = new TextField( "Id: " );
id.setWidth( 100 , Unit.PERCENTAGE );
id.setValue( this.cat.getId().toString() );
VerticalLayout layout = new VerticalLayout();
layout.addComponent( name );
layout.addComponent( id );
return layout;
}
private Layout buildLayoutForNoCat ( UUID uuid ) {
VerticalLayout layout = new VerticalLayout();
String message = "No cat found for the id: " + uuid;
Label label = new Label( message );
layout.addComponentsAndExpand( label );
return layout;
}
}
Cat
package com.basilbourque.example;
import java.util.UUID;
public class Cat {
private UUID id;
private String name;
public Cat ( UUID id , String name ) {
this.id = id;
this.name = name;
}
public UUID getId () {
return id;
}
public void setId ( UUID id ) {
this.id = id;
}
public String getName () {
return name;
}
public void setName ( String name ) {
this.name = name;
}
// Override `Object`.
#Override
public String toString () {
return "Cat{ " +
"id=" + id +
", name='" + name + '\'' +
" }";
}
}
DatabaseService
package com.basilbourque.example;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
// Pretending to be our gateway to a database.
public class DatabaseService {
static private List< Cat > cats;
{
DatabaseService.cats = List.of( // Produces an unmodifiable list. (A new feature in Java 9 and later.)
new Cat( UUID.fromString( "adf5c1a0-912e-11e8-9eb6-529269fb1459" ) , "Fluffy" ) ,
new Cat( UUID.fromString( "d37401c6-912e-11e8-9eb6-529269fb1459" ) , "Spot" ) ,
new Cat( UUID.fromString( "de29b6d8-912e-11e8-9eb6-529269fb1459" ) , "Lilly Mae" )
);
}
public List< Cat > fetchAllCats () {
return new ArrayList<>( DatabaseService.cats ); // Copy the list, then return.
}
public Optional< Cat > fetchCat ( UUID uuid ) {
return DatabaseService.cats.stream().filter( cat -> cat.getId().equals( uuid ) ).findFirst();
}
public static void main ( String[] args ) {
Optional< Cat > cat = new DatabaseService().fetchCat( UUID.fromString( "de29b6d8-912e-11e8-9eb6-529269fb1459" ) );
if ( cat.isPresent() ) {
System.out.println( "cat: " + cat.get() );
} else {
System.out.println( "No cat found." );
}
}
}

In log4j2, how to configure renameEmptyFiles to be false for the RollingFile appender?

I'm using log4j 2 and RollingFile appender:
<RollingFile name="mylog"
fileName="mylog.log"
filePattern="mylog.log.%d{yyyy-MM-dd}.log">
<PatternLayout>
<pattern>[%d] [%-5p] [%-8t] %F:%L %m%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
</RollingFile>
The log files do get renamed daily. But the Javadoc of FileRenameAction class indicates there is an option renameEmptyFiles which is false by default so if a day's log is empty it deletes it instead of rename it appending the date to the file name. How to configure it to true since I'd like to have the log file even if it's empty?
I made a little plugin that offers the desired functionality. I simply extended the DefaultRolloverStrategy and replaced (as all of its fields are final) the RolloverDescription object that is returned from rollover(). I copied the static #PluginFactory code from DefaultRolloverStrategy as it's required for the Log4j 2 plugin system.
Here's the code:
import java.util.zip.Deflater;
import org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy;
import org.apache.logging.log4j.core.appender.rolling.RollingFileManager;
import org.apache.logging.log4j.core.appender.rolling.RolloverDescription;
import org.apache.logging.log4j.core.appender.rolling.RolloverDescriptionImpl;
import org.apache.logging.log4j.core.appender.rolling.action.Action;
import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.util.Integers;
#Plugin( name = "KeepEmptyFilesRolloverStrategy", category = "Core", printObject = true )
public class KeepEmptyFilesRolloverStrategy extends DefaultRolloverStrategy
{
private static final int MIN_WINDOW_SIZE = 1;
private static final int DEFAULT_WINDOW_SIZE = 7;
#PluginFactory
public static KeepEmptyFilesRolloverStrategy createStrategy( #PluginAttribute( "max" ) final String max,
#PluginAttribute( "min" ) final String min,
#PluginAttribute( "fileIndex" ) final String fileIndex,
#PluginAttribute( "compressionLevel" ) final String compressionLevelStr,
#PluginElement( "Actions" ) final Action[] customActions,
#PluginAttribute( value = "stopCustomActionsOnError", defaultBoolean = true ) final boolean stopCustomActionsOnError,
#PluginConfiguration final Configuration config )
{
final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase( "max" );
int minIndex = MIN_WINDOW_SIZE;
if ( min != null )
{
minIndex = Integer.parseInt( min );
if ( minIndex < 1 )
{
LOGGER.error( "Minimum window size too small. Limited to " + MIN_WINDOW_SIZE );
minIndex = MIN_WINDOW_SIZE;
}
}
int maxIndex = DEFAULT_WINDOW_SIZE;
if ( max != null )
{
maxIndex = Integer.parseInt( max );
if ( maxIndex < minIndex )
{
maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
LOGGER.error( "Maximum window size must be greater than the minimum windows size. Set to "
+ maxIndex );
}
}
final int compressionLevel = Integers.parseInt( compressionLevelStr, Deflater.DEFAULT_COMPRESSION );
return new KeepEmptyFilesRolloverStrategy( minIndex,
maxIndex,
useMax,
compressionLevel,
config.getStrSubstitutor(),
customActions,
stopCustomActionsOnError );
}
protected KeepEmptyFilesRolloverStrategy( int minIndex,
int maxIndex,
boolean useMax,
int compressionLevel,
StrSubstitutor subst,
Action[] customActions,
boolean stopCustomActionsOnError )
{
super( minIndex, maxIndex, useMax, compressionLevel, subst, customActions, stopCustomActionsOnError );
}
#Override
public RolloverDescription rollover( final RollingFileManager manager ) throws SecurityException
{
RolloverDescription oldResult = super.rollover( manager );
// Fail fast (ClassCastException) if implementation of DefaultRolloverStrategy
// ever changes and uses a different Action type.
FileRenameAction oldRenameAction = (FileRenameAction) oldResult.getSynchronous();
FileRenameAction newRenameAction = new FileRenameAction( oldRenameAction.getSource(),
oldRenameAction.getDestination(),
true );
RolloverDescription newResult = new RolloverDescriptionImpl( oldResult.getActiveFileName(),
oldResult.getAppend(),
newRenameAction,
oldResult.getAsynchronous() );
return newResult;
}
}
To use this class, simply reference it in the Log4j 2 XML configuration, e.g. like this:
<RollingFile name="RollingFile" fileName="/usr/local/glassfish4.1-webprofile/glassfish/domains/domain1/logs/server.log" filePattern="/usr/local/glassfish4.1-webprofile/glassfish/domains/domain1/logs/server.%d{yyyyMMdd-HH:mm}.log">
<KeepEmptyFilesRolloverStrategy/>
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<CronTriggeringPolicy schedule="0 * * * * ?"/>
</RollingFile>
The implementation was inspired by this related answer.
On a sidenote, it might be necessary to use the new CronTriggeringPolicy to have empty log files being created at all, as it uses a separate thread. Judging from some other answers on SO, at least some of the other policies cannot react to the trigger as long as the Appender doesn't write out anything.

using Jackson for parsing retrofit response is a better option than parsing it in a custom way?

i am using retrofit api for fetching response but i have removed use of JACKSON api , from parsing section, for parsing i am using basic json parsing method, using own parsing class ( by using string convertor in Retrofit code )
So i have added Convertor like
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(API_URL)
.setConverter(new StringConverter())
.build();
I have removed JACKSON Api use like
#Override
public void success(String arg0, Response arg1) {
printMsg (arg1.getUrl()) ;
Dictionary<String, Dictionary<String, String>> receivedData = new StoreClass_parse(arg0.toString()).parseJsonData() ;
for ( int i = 0 ; i < receivedData.size() ; i++ )
{
printMsg ( receivedData.get(Integer.toString(i)).get("id")) ;
}

how to register a custom json marshaller in grails

I am trying to register a custom json marshaller like this
JSON.createNamedConfig("dynamic",{
def m = new CustomJSONSerializer()
JSON.registerObjectMarshaller(Idf, 1, { instance, converter -> m.marshalObject(instance, converter) })
})
and then using it like this
JSON.use("dynamic"){
render inventionList as JSON
}
but I am not sure if my custom serializer is being used because when I am debugging
control never goes to marshalObject function of my custom serializer
My custom serializer is as follows
import grails.converters.deep.JSON
import java.beans.PropertyDescriptor
import java.lang.reflect.Field
import java.lang.reflect.Method
import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException
import org.codehaus.groovy.grails.web.converters.marshaller.json.GroovyBeanMarshaller
import org.codehaus.groovy.grails.web.json.JSONWriter
class CustomJSONSerializer extends GroovyBeanMarshaller{
public boolean supports(Object object) {
return object instanceof GroovyObject;
}
public void marshalObject(Object o, JSON json) throws ConverterException {
JSONWriter writer = json.getWriter();
println 'properties '+BeanUtils.getPropertyDescriptors(o.getClass())
for(PropertyDescriptor property:BeanUtils.getProperyDescriptors(o.getClass())){
println 'property '+property.getName()
}
try {
writer.object();
for (PropertyDescriptor property : BeanUtils.getPropertyDescriptors(o.getClass())) {
String name = property.getName();
Method readMethod = property.getReadMethod();
if (readMethod != null && !(name.equals("metaClass")) && readMethod.getName()!='getSpringSecurityService') {
Object value = readMethod.invoke(o, (Object[]) null);
writer.key(name);
json.convertAnother(value);
}
}
for (Field field : o.getClass().getDeclaredFields()) {
int modifiers = field.getModifiers();
if (Modifier.isPublic(modifiers) && !(Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers))) {
writer.key(field.getName());
json.convertAnother(field.get(o));
}
}
writer.endObject();
}
catch (ConverterException ce) {
throw ce;
}
catch (Exception e) {
throw new ConverterException("Error converting Bean with class " + o.getClass().getName(), e);
}
}
}
Is it possible to debug the serializer? If not then how can I exclude a property from serialization? There's some property which throws exception during serialization.
this is what i do for custom JSON marshaling in the Bootstrap init closure:
def init = {servletContext ->
grailsApplication.domainClasses.each {domainClass ->
domainClass.metaClass.part = {m ->
def map = [:]
if (m.'include') {
m.'include'.each {
map[it] = delegate."${it}"
}
} else if (m.'except') {
m.'except'.addAll excludedProps
def props = domainClass.persistentProperties.findAll {
!(it.name in m.'except')
}
props.each {
map['id'] = delegate.id
map[it.name] = delegate."${it.name}"
}
}
return map
}
}
JSON.registerObjectMarshaller(Date) {
return it?.format("dd.MM.yyyy")
}
JSON.registerObjectMarshaller(User) {
def returnArray = [:]
returnArray['username'] = it.username
returnArray['userRealName'] = it.userRealName
returnArray['email'] = it.email
return returnArray
}
JSON.registerObjectMarshaller(Role) {
def returnArray = [:]
returnArray['authority'] = it.authority
return returnArray
}
JSON.registerObjectMarshaller(Person) {
return it.part(except: ['fieldX', 'fieldY'])
}}
you see that i have custom marshallers for the Date, Use, Person, and Role Class
I found this post while trying to find out a proper way for the marshaller registering. The biggest difference compared to Nils' answer is that this solution leaves BootStrap.groovy intact, which is nice.
http://compiledammit.com/2012/08/16/custom-json-marshalling-in-grails-done-right/
To customize Json serialization , the best practices is to use a grails plugin,such as the following :
http://grails.org/plugin/marshallers