0

When I try to bind a JSON payload on to an existing domain object that has a nullable reference to another domain object, the binding fails if the in-bound JSON indicates null for the reference. This causes the persistence attempt to fail with this error:

| Error 2011-12-21 10:21:24,202 ["http-bio-8080"-exec-3] ERROR hibernate.AssertionFailure  - an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session)
Message: null id in server.Uploader entry (don't flush the Session after an exception occurs)
    Line | Method
->>  105 | update    in server.SessionController
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|   1110 | runWorker in java.util.concurrent.ThreadPoolExecutor
|    603 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^    722 | run       in java.lang.Thread
| Error 2011-12-21 10:21:24,206 ["http-bio-8080"-exec-3] ERROR errors.GrailsExceptionResolver  - AssertionFailure occurred when processing request: [POST] /Server/session/2
null id in server.Uploader entry (don't flush the Session after an exception occurs). Stacktrace follows:
Message: null id in server.Uploader entry (don't flush the Session after an exception occurs)
    Line | Method
->>  105 | update    in server.SessionController
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|   1110 | runWorker in java.util.concurrent.ThreadPoolExecutor
|    603 | run . . . in java.util.concurrent.ThreadPoolExecutor$Worker
^    722 | run       in java.lang.Thread

Here's the domain objects in question:

class Session {
    DateTime dateCreated
    DateTime lastUpdated
    String ip
    String state

    static hasOne = [uploader:Uploader]
    static constraints = {
        ip blank: false
        state blank: false
        uploader nullable: true, unique: true
    }
}

class Uploader {
    DateTime dateCreated
    DateTime lastUpdated
    Session session
    String firstName
    String lastName
    String organization
    String phoneNumber
    String emailAddress

    static constraints = {
        firstName blank: false
        lastName blank: false
        organization blank: false
    }
}

Here's the controller code that throws the error:

def update() {
    def sessionInstance = Session.get(params.id)
    if (!sessionInstance) {
        flash.message = message(code: 'default.not.found.message', args: [message(code: 'session.label', default: 'Session'), params.id])
        redirect(action: "list")
        return
    }

    if (params.version) {
        def version = params.version.toLong()
        if (sessionInstance.version > version) {
            sessionInstance.errors.rejectValue("version", "default.optimistic.locking.failure",
                    [message(code: 'session.label', default: 'Session')] as Object[],
                    "Another user has updated this Session while you were editing")
            render(view: "edit", model: [sessionInstance: sessionInstance])
            return
        }
    }

    sessionInstance.properties = params

    if (!sessionInstance.save(flush: true)) {      // <---- Error thrown here
        render(view: "edit", model: [sessionInstance: sessionInstance])
        return
    }

    flash.message = message(code: 'default.updated.message', args: [message(code: 'session.label', default: 'Session'), sessionInstance.id])
    redirect(action: "show", id: sessionInstance.id)
}

This is the JSON payload that causes problems:

{
"id":2,
"dateCreated":"2011-12-21T09:33:33-06:00",
"ip":"127.0.0.1",
"lastUpdated":"2011-12-21T09:33:33-06:00",
"state":"closed",
"uploader":null
}

but this JSON payload works just fine:

{
"class": "server.Session",
"id":2,
"dateCreated":"2011-12-21T09:33:33-06:00",
"ip":"127.0.0.1",
"lastUpdated":"2011-12-21T09:33:33-06:00",
"state":"closed",
"uploader":null
}

What happens is that the uploader property of the session is getting set to "server.Uploader : null". This is the .dump() of the sessionInstance object after sessionInstance.properties = params:

<server.Session@713c6968 dateCreated=2011-12-21T09:33:33.000-06:00 lastUpdated=2011-12-21T09:33:33.000-06:00 ip=127.0.0.1 state=closed errors=grails.validation.ValidationErrors: 0 errors id=2 version=1 uploader=server.Uploader : null>

Everything works correctly when the "class": "server.Session" property is in the JSON payload, but without it, everything breaks down. I would think that the data binding would be able to handle this, since it maps the rest of the properties just fine, but seems to just fail horribly for the reference.

Lastly, the reason I ran into this is because I am trying to build a Griffon client against a REST API, and it seems that the HTTPBuilder is stripping out the "class": "server.Session" when it converts the original JSON response into a net.sf.json.JSONObject, since the over-the-wire response has the property in it, but the dump of the net.sf.json.JSONObject doesn't, nor does the subsequent JSON request payload.

  1. Is this a bug in how Grails data-binds when there is no "class" property of a JSON object?
  2. Is this a bug in how HTTPBuilder parses a JSON response?
  3. Is it a bug in how HTTPBuilder outputs JSON from a net.sf.json.JSONObject?
  4. Am I just doing something completely wrong?

Update

I have found a few additional bits of information:

  1. Removing the unique constraint doesn't change the behavior
  2. If the uploader field is non-null and the in-bound JSON object is trying to set it to null, the property is not changed (and no error occurs). But, if the "class": "server.Session" property is in the in-bound JSON payload, it changes to null as expected.
cdeszaq
  • 30,869
  • 25
  • 117
  • 173

1 Answers1

0

IIRC Json-lib's JSONObject will automatically strip special properties like "class" and "metaClass" (in the case of a POGO). You can configure this behavior by means of a JsonConfig instance, however I'm unaware if HTTPBuilder allows you to inject such instance at the appropriate time.

An alternative (kind of hacky) would be to send the class property value under a different name (say clazz) which Json-lib won't strip. You'll have to register a filter on the Grails side to map 'clazz' into 'class'.

Andres Almiray
  • 3,236
  • 18
  • 28
  • Yes, that would function as a workaround, but shouldn't Grails's data binding be smart enough to set the property to null, instead of `"server.Uploader : null"`? It clearly knows _what_ property it is working with, since it figured out the `server.Uploader` part, which is the type of that property, and it knows the value (null), but it just isn't quite finishing the job. – cdeszaq Dec 21 '11 at 18:39
  • Results of removing unique constraint, as well as some additional info, added to the end of my question. – cdeszaq Dec 21 '11 at 19:07