Why is Groovy copying objects without prompting and reassigning fields?

58 Views Asked by At

This is so bizarre I have no idea how to even properly formulate this question, but hopefully some guru recognises what is going on here and can help me.

I am working on a Grails project that is exhibiting a bizarre bug. While marshalling a controller's response to JSON, it throws this exception:

Caused by: org.grails.web.json.JSONException: Misplaced key: expected mode of KEY but was OBJECT
    at org.grails.web.json.JSONWriter.key(JSONWriter.java:237)
    at org.grails.web.converters.marshaller.json.DomainClassMarshaller.marshalObject(DomainClassMarshaller.java:169)
    at org.grails.web.converters.marshaller.json.DomainClassMarshaller.marshalObject(DomainClassMarshaller.java:59)
    at grails.converters.JSON.value(JSON.java:184)
    ... 55 common frames omitted

The complete object tree it is marshalling is an ArrayList of Form objects, and at this point it is marshalling a Form object, specifically it's adding its primary key as a property to the JSON object.

Here is the relevant code from DomainClassMarshaller:

    public void marshalObject(Object value, JSON json) throws ConverterException {
        JSONWriter writer = json.getWriter();

        // (...)

        writer.object();

        // (...)

        PersistentProperty id = domainClass.getIdentity();
        if(id != null) {
            if(shouldInclude(includeExcludeSupport, includes, excludes, value, id.getName())) {
                Object idValue = extractValue(value, id);
                if(idValue != null) {
                    json.property(id.getName(), idValue); // <-- JSONException thrown here
                }
            }    
        }

        // (...)
    }

And here are JSONObject.getWriter():

    public JSONWriter getWriter() throws ConverterException {
        return writer;
    }

...and JSONObject.property():

    public void property(String key, Object value) throws JSONException, ConverterException {
        writer.key(key); // <-- Throws JSONException if writer.mode is not KEY
        value(value);
    }

So the exception happens when the json.property(id.getName(), idValue) is invoked, because at that point json.writer is in mode OBJECT, while it expects to be in mode KEY.

But here is the bizarre part: even though writer == json.writer, writer is in mode KEY while json.writer is in mode OBJECT!!! It has been copied somehow, at some point, while its mode was still OBJECT, so that now it is in the wrong state and an exception is thrown.

I have verified this with a debugger. Even though the code does writer = json.getWriter(), and JSON.getWriter() just does return writer, after that lines returns, writer is a different instance than json.writer! I can tell this from the object IDs. And in fact, the identity of json.writer keeps changing after that!

I have no idea how this is possible. I can see it happen in a debugger. I have set a "when modified" breakpoint on the JSONObject.writer field to see who or what is changing it, but that is never hit. I have set a breakpoint in the constructor of JSONWriter to see who or what is making all these copies, but that too is never hit beyond the first time.

Here is the code again, with some annotations added to illustrate what I see in a debugger when I step through it. I'm pretty confident that the same thing happens when not running in a debugger, because exactly the same exception is thrown:

    public void marshalObject(Object value, JSON json) throws ConverterException {
        // json.writer == JSONWriter@123
        JSONWriter writer = json.getWriter();
        // writer == JSONWriter@134
        // json.writer == JSONWriter@145

        // (...)

        writer.object();
        // writer == JSONWriter@134
        // writer.mode == KEY
        // json.writer == JSONWriter@156
        // json.writer.mode == OBJECT

        // (...)

        PersistentProperty id = domainClass.getIdentity();
        if(id != null) {
            if(shouldInclude(includeExcludeSupport, includes, excludes, value, id.getName())) {
                Object idValue = extractValue(value, id);
                if(idValue != null) {
                    // This should work, since writer should == json.writer, and writer.mode == KEY,
                    // but instead json.writer is now a different instance of JSONWriter,
                    // and json.writer.mode is still OBJECT:
                    json.property(id.getName(), idValue); // <-- JSONException thrown here
                }
            }    
        }
        // (...)
    }

Please help me figure out what on Earth is going on here!

Some versions:
Grails: 5.2.4
Groovy: 3.0.11
Java: Eclipse Adoptium Temurin-11.0.18+10

0

There are 0 best solutions below