1

In the context of a Grails application, we parse JSON into command objects. The automatic conversion from a JSON map to the POGO fails with an error like this:

org.codehaus.groovy.runtime.typehandling.GroovyCastException:
Cannot cast object '{<snip>}' with class 'groovy.json.internal.LazyMap' to class 'SomeCmd' due to:
java.lang.IllegalArgumentException: No enum constant Foo.my-bar

I narrowed it down to this plain Groovy MWE:

import groovy.json.JsonSlurper

enum Foo {
    Bar("my-bar"),
    Ista("my-ista")

    final String s

    private Foo(String s) {
        this.s = s
    }
}

class SomeCmd {
    Foo foo
}

def some = new SomeCmd(new JsonSlurper().parseText('{ "foo" : "my-bar" }'))
println(some.foo)

This errors with

java.lang.IllegalArgumentException: No enum constant Foo.my-bar

This is expected -- so far, so good.

Now, following the documentation, I thought adding custom coercion from String to Foo might resolve the issue (also from here):

enum Foo {
    <snip>

    static Foo fromJsonString(String s) {
        return values().find { it.s == s }
    }
}

def oldAsType = String.metaClass.getMetaMethod("asType", [Class] as Class[])
String.metaClass.asType = { Class type ->
    type == Foo ?
            Foo.byJsonString(delegate as String) :
            oldAsType.invoke(delegate, [type] as Class[])
}

However, the error persists. Apparently, JsonSlurper does not use coercion at all, given that

println("my-bar" as Foo)

prints Bar as desired.

What is going on here? How can I get JsonSlurper to pick the correct enum cases by something besides the case name?


PS: Fun fact, if we change the second-to-last line to

new JsonSlurper().parseText('{ "foo" : "my-bar" }') as SomeCmd

the script prints null.

Raphael
  • 9,779
  • 5
  • 63
  • 94

1 Answers1

0

Groovy will happily use custom setters to construct the object. With Foo.fromJsonString as given in the question, define SomeCmd like so:

class SomeCmd {
    Foo foo

    void setFoo(Object jsonObject) {
        if (jsonObject == null || jsonObject instanceof Foo) {
            this.foo = jsonObject
            return
        } else if (jsonObject instanceof String) {
            Foo f = Foo.fromJsonString(jsonObject)
            if ( null != f ) {
                this.foo = f
                return
            }
        }

        throw new IllegalArgumentException("No Foo for $jsonObject")
    }
}

Then, the code as given prints Bar as desired.

However, this doesn't help in Grails with parsing JSON into command objects -- Grails doesn't use coercion nor Groovy's map "magic". See this follow-up question.

Raphael
  • 9,779
  • 5
  • 63
  • 94