That code seems to be correct. I used these steps to recreate the project:
grails create-app com.demo.Person
grails create-domain-class Person
grails create-controller Person
Added an attribute in the Person domain class and your add-method to the controller:
package com.demo.person
class Person {
String name
}
package com.demo.person
import grails.converters.JSON
class PersonController {
def index() { }
def add() {
def person = new Person(name: params.name)
if (person.validate() && person.save(flush: true)) {
render([success:true, data:[id:person.id, name:person.name]] as JSON)
} else {
render([success:false, errors:person.errors.allErrors.collect{ it.defaultMessage }] as JSON)
}
}
}
Starting the app with grails run-app and then making the post with the name param in the url (here with httpie). If you're posting JSON you should use the request.JSON instead of params.
http -v POST "http://localhost:8080/com.demo.Person/person/add?name=HolyGrail"
This is the result:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Date: Wed, 15 Oct 2025 11:55:54 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
{
"data": {
"id": 1,
"name": "HolyGrail"
},
"success": true
}
If you try these steps, do you get the correct result? Do you have any other setup in your project that might interfer? Compare your project files with the new project. Does grails url-mappings-report give you the expected result? You should have something like the default:
Dynamic Mappings
| * | /${controller}/${action}?/${id}?(.${format)? | Action: (default action) |