I have a controller with the POST handler defined like so:
@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @Valid UIVendor vendor,
BindingResult result,
Locale currentLocale )
var vendor =
{
vendorId: 123,
vendorName: "ABC Company",
emails : [
{ emailAddress: "abc123@abc.com", flags: 2 },
{ emailAddress: "xyz@abc.com", flags: 3 }
]
}
$.post("ajax/saveVendor.do", $.param(vendor), saveEntityCallback, "json" );
Invalid property 'emails[0][emailAddress]' of bean class [beans.UIVendor]: Property referenced in indexed property path 'emails[0][emailAddress]' is neither an array nor a List nor a Map; returned value was [abc123@abc.com]
package com.mycompany.beans;
import java.util.*;
import org.apache.commons.lang.*;
import com.mycompany.domain.Vendor;
import com.mycompany.domain.VendorAttributes;
import org.apache.commons.logging.*;
import org.codehaus.jackson.annotate.JsonIgnore;
public class UIVendor
{
private final Log logger = LogFactory.getLog( this.getClass() );
private Vendor vendor;
private boolean ftpFlag;
private String ftpHost;
private String ftpPath;
private String ftpUser;
private String ftpPassword;
private List<UINotificationEmail> emails = null;
public UIVendor() { this( new Vendor() ); }
public UIVendor( Vendor vendor )
{
this.vendor = vendor;
loadVendorAttributes();
}
private void loadVendorAttributes()
{
this.ftpFlag = false;
this.ftpHost = this.ftpPassword = this.ftpPath = this.ftpUser = "";
this.emails = null;
for ( VendorAttributes a : this.vendor.getVendorAttributes() )
{
String key = a.getVendorFakey();
String value = a.getVendorFaValue();
int flags = a.getFlags();
if ( StringUtils.isBlank(key) || StringUtils.isBlank(value) ) continue;
if ( key.equals( "ftpFlag" ) )
{
this.ftpFlag = BooleanUtils.toBoolean( value );
}
else if ( key.equals( "ftpHost" ) )
{
this.ftpHost = value;
}
else if ( key.equals("ftpPath") )
{
this.ftpPath = value;
}
else if ( key.equals("ftpUser") )
{
this.ftpUser = value;
}
else if ( key.equals("ftpPassword") )
{
this.ftpPassword = value;
}
else if ( key.equals("email") )
{
UINotificationEmail email = new UINotificationEmail(value, flags);
this.getEmails().add( email );
}
}
}
private void saveVendorAttributes()
{
int id = this.vendor.getVendorId();
List<VendorAttributes> attrs = this.vendor.getVendorAttributes();
attrs.clear();
if ( this.ftpFlag )
{
VendorAttributes flag = new VendorAttributes();
flag.setVendorId( id );
flag.setStatus( "A" );
flag.setVendorFakey( "ftpFlag" );
flag.setVendorFaValue( BooleanUtils.toStringTrueFalse( this.ftpFlag ) );
attrs.add( flag );
if ( StringUtils.isNotBlank( this.ftpHost ) )
{
VendorAttributes host = new VendorAttributes();
host.setVendorId( id );
host.setStatus( "A" );
host.setVendorFakey( "ftpHost" );
host.setVendorFaValue( this.ftpHost );
attrs.add( host );
if ( StringUtils.isNotBlank( this.ftpPath ) )
{
VendorAttributes path = new VendorAttributes();
path.setVendorId( id );
path.setStatus( "A" );
path.setVendorFakey( "ftpPath" );
path.setVendorFaValue( this.ftpPath );
attrs.add( path );
}
if ( StringUtils.isNotBlank( this.ftpUser ) )
{
VendorAttributes user = new VendorAttributes();
user.setVendorId( id );
user.setStatus( "A" );
user.setVendorFakey( "ftpUser" );
user.setVendorFaValue( this.ftpUser );
attrs.add( user );
}
if ( StringUtils.isNotBlank( this.ftpPassword ) )
{
VendorAttributes password = new VendorAttributes();
password.setVendorId( id );
password.setStatus( "A" );
password.setVendorFakey( "ftpPassword" );
password.setVendorFaValue( this.ftpPassword );
attrs.add( password );
}
}
}
for ( UINotificationEmail e : this.getEmails() )
{
logger.debug("Adding email " + e );
VendorAttributes email = new VendorAttributes();
email.setStatus( "A" );
email.setVendorFakey( "email" );
email.setVendorFaValue( e.getEmailAddress() );
email.setFlags( e.getFlags() );
email.setVendorId( id );
attrs.add( email );
}
}
@JsonIgnore
public Vendor getVendor()
{
saveVendorAttributes();
return this.vendor;
}
public int getVendorId()
{
return this.vendor.getVendorId();
}
public void setVendorId( int vendorId )
{
this.vendor.setVendorId( vendorId );
}
public String getVendorType()
{
return this.vendor.getVendorType();
}
public void setVendorType( String vendorType )
{
this.vendor.setVendorType( vendorType );
}
public String getVendorName()
{
return this.vendor.getVendorName();
}
public void setVendorName( String vendorName )
{
this.vendor.setVendorName( vendorName );
}
public String getStatus()
{
return this.vendor.getStatus();
}
public void setStatus( String status )
{
this.vendor.setStatus( status );
}
public boolean isFtpFlag()
{
return this.ftpFlag;
}
public void setFtpFlag( boolean ftpFlag )
{
this.ftpFlag = ftpFlag;
}
public String getFtpHost()
{
return this.ftpHost;
}
public void setFtpHost( String ftpHost )
{
this.ftpHost = ftpHost;
}
public String getFtpPath()
{
return this.ftpPath;
}
public void setFtpPath( String ftpPath )
{
this.ftpPath = ftpPath;
}
public String getFtpUser()
{
return this.ftpUser;
}
public void setFtpUser( String ftpUser )
{
this.ftpUser = ftpUser;
}
public String getFtpPassword()
{
return this.ftpPassword;
}
public void setFtpPassword( String ftpPassword )
{
this.ftpPassword = ftpPassword;
}
public List<UINotificationEmail> getEmails()
{
if ( this.emails == null )
{
this.emails = new ArrayList<UINotificationEmail>();
}
return emails;
}
public void setEmails(List<UINotificationEmail> emails)
{
this.emails = emails;
}
}
{
"vendorName":"MAIL",
"vendorId":45,
"emails":
[
{
"emailAddress":"dfg",
"success":false,
"failure":false,
"flags":0
}
],
"vendorType":"DFG",
"ftpFlag":true,
"ftpHost":"kdsfjng",
"ftpPath":"dsfg",
"ftpUser":"sdfg",
"ftpPassword":"sdfg",
"status":"A"
}
{
"vendorId":"45",
"vendorName":"MAIL",
"vendorType":"DFG",
"ftpFlag":true,
"ftpHost":"kdsfjng",
"ftpUser":"sdfg",
"ftpPath":"dsfg",
"ftpPassword":"sdfg",
"status":"A",
"emails":
[
{
"success":"false",
"failure":"false",
"emailAddress":"dfg"
},
{
"success":"true",
"failure":"true",
"emailAddress":"pfc@sj.org"
}
]
}
Update: since Spring 3.1, it's possible to use @Valid On @RequestBody Controller Method Arguments.
@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @Valid @RequestBody UIVendor vendor,
BindingResult result,
Locale currentLocale )
After much trial and error, I've finally figured out, as well as I can, what the problem is. When using the following controller method signature:
@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @Valid UIVendor vendor,
BindingResult result,
Locale currentLocale )
The client script has to pass the field in the object in post-data (typically "application/x-www-form-urlencoded") format (i.e., field=value&field2=value2). This is done in jQuery like this:
$.post( "mycontroller.do", $.param(object), callback, "json" )
This works fine for simple POJO objects that don't have child objects or collections, but once you introduce significant complexity to the object being passed, the notation used by jQuery to serialize the object data is not recognized by Spring's mapping logic:
object[0][field]
The way that I solved this problem was to change the method signature in the controller to:
@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @RequestBody UIVendor vendor,
Locale currentLocale )
And change the call from client to:
$.ajax(
{
url:"ajax/mycontroller.do",
type: "POST",
data: JSON.stringify( objecdt ),
success: callback,
dataType: "json",
contentType: "application/json"
} );
This requires the use of the JSON javascript library. It also forces the contentType to "application/json", which is what Spring expects when using the @RequestBody annotation, and serializes the object to a format that Jackson can deserialize into a valid object structure.
The only side effect is that now I have to handle my own object validation inside of the controller method, but that's relatively simple:
BindingResult result = new BeanPropertyBindingResult( object, "MyObject" );
Validator validator = new MyObjectValidator();
validator.validate( object, result );
If anyone has any suggestions to improve upon this process, I'm all ears.