grailsgrails-2.4

RestfulController not binding nested JSON in POST


I'm building an REST API in Grails 2.4.4 and relying on RestfulController to handle basic CRUD type functionality. The front end is being built in AngularJS 1.3 and for the most part Angular's $resource and Grails RestfulController work perfectly and don't require that I write a lot of boilerplate code. I have one major problem though which is that a domain object with a hasMany relationship doesn't bind as expected when I POST the JSON from Angular to Grails.

For example take the following domain objects:

class Task {

    String name
    //auto timestamps
    Date dateCreated
    Date lastUpdated

    static hasMany = [filters:TaskFilter]

    static constraints = {
    }

}

class TaskFilter {

  String filterMetaData 

  static belongsTo = [task:Task]

  static constraints = {
    task column: 'task_id'
  }
}

When I POST an object like this for example:

{name: "Task 1", filters: [{filterMetaData:'some-meta-data'}]}

I get the following error in Grails:

| Error 2015-02-10 17:41:34,619 [http-bio-8080-exec-5] ERROR errors.GrailsExceptionResolver  - NullPointerException occurred when processing request: [POST] /api/studies/1/tasks
Stacktrace follows:
Message: null
    Line | Method
->>   99 | $tt__save          in grails.rest.RestfulController
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|    198 | doFilter           in grails.plugin.cache.web.filter.PageFragmentCachingFilter
|     63 | doFilter . . . . . in grails.plugin.cache.web.filter.AbstractFilter
|    104 | processFilterChain in com.odobo.grails.plugin.springsecurity.rest.RestTokenValidationFilter
|     71 | doFilter . . . . . in     ''
|     53 | doFilter           in grails.plugin.springsecurity.web.filter.GrailsAnonymousAuthenticationFilter
|    122 | doFilter . . . . . in com.odobo.grails.plugin.springsecurity.rest.RestAuthenticationFilter
|     82 | doFilter           in grails.plugin.springsecurity.web.authentication.logout.MutableLogoutFilter
|     63 | doFilter . . . . . in com.odobo.grails.plugin.springsecurity.rest.RestLogoutFilter
|     82 | doFilter           in com.brandseye.cors.CorsFilter
|   1145 | runWorker . . . .  in java.util.concurrent.ThreadPoolExecutor
|    615 | run                in java.util.concurrent.ThreadPoolExecutor$Worker
^    745 | run . . . . . . .  in java.lang.Thread

I'm not exactly sure what is going on with this error as it's pretty vague but the Task and the TaskFilter are rolled back and nothing is saved.

The weird thing is that if I get rid of this line of code from TaskFilter:

static belongsTo = [task:Task]

Everything works fine EXCEPT that this then forces me to have a task_task_filter join table instead of the much easier to grok task_id column in task_filter table.

I've been reading posts here and on various forums, blogs and mailing lists for the last couple of days and have not found a solution. Most of the posts are older so I can't tell if they apply but several simply say "You can't bind a JSONArray using Grails" which seems bizarre as it's such a common requirement for any moderately complex data model.

Anyway - if anyone can point me to something concrete and current that either says "you can't do it" or how to actually handle this situation I'd greatly appreciate it. If possible I'd rather avoid having to write custom parsing code to manage this as the rest of RestfulController works like a charm. Also if there is any explanation as why this works when I removed the belongsTo side of the relationship maybe that will help me understand what is going on here.

Just in case anyone is wondering my RestfulController subclass looks like this:

class TaskController extends RestfulController<Task> {

  static responseFormats = ['json', 'xml']

  TaskController() {
    super(Task)
  }
}

Solution

  • After much discussion with the grails dev team I've been informed that my understanding of how this should work is not the way Grails works. I assumed (incorrectly) that because the data binding added the TaskFilter to the Task that it would behave as if addToFilters() was called on the Task but for whatever that's not how the collection is created. I don't know what the collection actually is but it definitely does not save them..

    The JIRA has been Closed as "behaves as expected".

    My workaround after this is the following in my TaskController

    @Override
    protected Task createResource() {
      Task task = super.createResource()
      if (task.filters) {
        task.filters.each {filter ->
          task.addToFilters(filter)
        }
      }
      task
    }
    

    Unfortunately any associations in a RestfulController based application will need this same type of code for handling nested resources. I still think Grails should handle this in the framework and I may try and add the feature to the code if I can find time to work on it.