I have observed that when I add more than 18 parameters to a Play Framework Form-class I get a long (and for me incomprehensible) compilation error.
Is this a documented limitation? I need to take in as much as 29 parameters in a form post. I don't decide on the design and number of parameters as I am implementing a protocol from an open standard.
I'm mapping like this:
val registration = Form(mapping(
"client_type" -> nonEmptyText,
"client_id" -> optional(nonEmptyText),
... up to 29 args, all optional(nonEmptyText)
){ (clientType, clientId ...) => RegistrationRequest(clientType, clientId ...) }
{ req => None })
My strategy was to do the mapping this way instead of apply/unapply and create an heirarchy of case classes. The reason is to work around the 22 arguments limit in Case classes, which was the first seemingly arbitrary limit I ran into. Up to 18 args mapping works, after that I get a long compilation error.
The error message can be found here (too long to include): https://gist.github.com/2928297
I'm looking for suggestions on how I can get around this limitation. I know it is bad design to send in 29 parameters in a form Post, but it should still be possible.
Hack/Workaround/Solution
Ok, here is my hacked together workaround (writing this post took much longer than implementing, I hacked for ~30min on this)
I wrote functions that preprocesses the request params and adds a group prefix to group certain params. I then use the resulting Map[String, String] and continue processing with the form class, doing validation etc as usual. This allows me to use nested case classes in the mapping and get below the 18 params limit.
Beware: ugly code ahead! I should probably not show early hacky code like this, but I'm hoping it will help someone else who wants a workaround.
def preprocessFormParams(prefix:String, replace:String)(implicit request:Request[AnyContent]):Map[String, String] = request.body.asFormUrlEncoded.map( _.filterKeys( _.startsWith(prefix)).map( m => m._1.patch(0, replace, prefix.length) -> m._2.head )).getOrElse(Map.empty)
def unprocessedFormParams(prefixes:Set[String])(implicit request:Request[AnyContent]):Map[String, String] = request.body.asFormUrlEncoded.map( _.filterKeys( !prefixes.contains(_) ).map( m => m._1 -> m._2.head )).getOrElse(Map.empty)
So these functions should probably be for comprehensions or split up, but here goes:
preprocessedFormParms takes a prefix and replaces it:
val clientParams = preprocessFormParams("client_", "client.")
("client_id" -> "val1", "client_type" -> "val2") becomes ("client.id" -> "val1", "client.type" -> "val2")
When I have the parameters in the form of group.key1, group.key2 I can nest the case classes in the form like so
Form(mapping("client" -> mapping("type" -> nonEmptyText
"id" -> optional(nonEmptyText),
"secret" -> optional(nonEmptyText))
(RegisterClient.apply)(RegisterClient.unapply)
... more params ...)
(RegisterRequest.apply)(RegisterRequest.unapply)
In my action I go ahead and filter out each of my groups
implicit request =>
val clientParams = preprocessFormParams("client_", "client.")
val applicationParams = preprocessFormParams("application_", "application.")
val unprocessedParams = unprocessedFormParams(Set("client_", "application_"))
val processedForm = clientParams ++ applicationParams ++ unprocessedParams
Lastly I can apply my form like normal but now I get the nested structure I that reduces the number of arguments and hopefully makes the case class more manageable.
clientRegistrationForm.bind(processedForm).fold( ... )
Using this approach you can keep the number of parameters down. If your parameters don't have the same prefix for easy grouping like my problem, then you can still use the same basic approach but filter on other criterias.
I had to work around this limitation the other day and didn't find this S.O post and came up with a different method of doing things that seems to work despite it looking a little wonky.
Our form components
import play.api.data.Form
import play.api.data.Forms._
case class P1_18(f1: String,f2: String,f3: String,f4: String,f5: String,f6: String,f7: String,f8: String,f9: String,f10: String,f11: String,f12: String,f13: String,f14: String,f15: String,f16: String,f17: String,f18: String)
case class P2_18(f1: String,f2: String,f3: String,f4: String,f5: String,f6: String,f7: String,f8: String,f9: String,f10: String,f11: String,f12: String,f13: String,f14: String,f15: String,f16: String,f17: String,f18: String)
case class P36(f1: String,f2: String,f3: String,f4: String,f5: String,f6: String,f7: String,f8: String,f9: String,f10: String,f11: String,f12: String,f13: String,f14: String,f15: String,f16: String,f17: String,f18: String,f19: String,f20: String,f21: String,f22: String,f23: String,f24: String,f25: String,f26: String,f27: String,f28: String,f29: String,f30: String,f31: String,f32: String,f33: String,f34: String,f35: String,f36: String)
P36 is the object you actually want, P1/P2 are just classes you use to build it up within the constraints of the framework, I made these private in my actual application to the object wrapping the form.
Then we have our form definition, this is where the magic happens:
val f = Form(
mapping(
"" -> mapping(
"f1" -> text,
"f2" -> text,
"f3" -> text,
"f4" -> text,
"f5" -> text,
"f6" -> text,
"f7" -> text,
"f8" -> text,
"f9" -> text,
"f10" -> text,
"f11" -> text,
"f12" -> text,
"f13" -> text,
"f14" -> text,
"f15" -> text,
"f16" -> text,
"f17" -> text,
"f18" -> text
)(P1_18.apply)(P1_18.unapply),
"" -> mapping(
"f19" -> text,
"f20" -> text,
"f21" -> text,
"f22" -> text,
"f23" -> text,
"f24" -> text,
"f25" -> text,
"f26" -> text,
"f27" -> text,
"f28" -> text,
"f29" -> text,
"f30" -> text,
"f31" -> text,
"f32" -> text,
"f33" -> text,
"f34" -> text,
"f35" -> text,
"f36" -> text
)(P2_18.apply)(P2_18.unapply)
)(
(p1, p2) =>
P36(
f1 = p1.f1,
f2 = p1.f2,
f3 = p1.f3,
f4 = p1.f4,
f5 = p1.f5,
f6 = p1.f6,
f7 = p1.f7,
f8 = p1.f8,
f9 = p1.f9,
f10 = p1.f10,
f11 = p1.f11,
f12 = p1.f12,
f13 = p1.f13,
f14 = p1.f14,
f15 = p1.f15,
f16 = p1.f16,
f17 = p1.f17,
f18 = p1.f18,
f19 = p2.f1,
f20 = p2.f2,
f21 = p2.f3,
f22 = p2.f4,
f23 = p2.f5,
f24 = p2.f6,
f25 = p2.f7,
f26 = p2.f8,
f27 = p2.f9,
f28 = p2.f10,
f29 = p2.f11,
f30 = p2.f12,
f31 = p2.f13,
f32 = p2.f14,
f33 = p2.f15,
f34 = p2.f16,
f35 = p2.f17,
f36 = p2.f18
)
)(
p => {
val p1 = P1_18(p.f1,p.f2,p.f3,p.f4,p.f5,p.f6,p.f7,p.f8,p.f9,p.f10,p.f11,p.f12,p.f13,p.f14,p.f15,p.f16,p.f17,p.f18)
val p2 = P2_18(p.f19,p.f20,p.f21,p.f22,p.f23,p.f24,p.f25,p.f26,p.f27,p.f28,p.f29,p.f30,p.f31,p.f32,p.f33,p.f34,p.f35,p.f36)
Option(
(p1,p2)
)
}
)
)
You might say: Huh. Uh, excuse me, you have an empty key bound twice. How could that possibly work? And I say:
val dataSeq = for(i <- 1 to 36) yield s"f${i}" -> s"text no. #${i}"
val filledFormFromMap = f.bind(dataSeq.toMap)
filledFormFromMap.value
// res9: Option[P36] = Some(P36(text no. #1,text no. #2,text no. #3,text no. #4,text no. #5,text no. #6,text no. #7,text no. #8,text no. #9,text no. #10,text no. #11,text no. #12,text no. #13,text no. #14,text no. #15,text no. #16,text no. #17,text no. #18,text no. #19,text no. #20,text no. #21,text no. #22,text no. #23,text no. #24,text no. #25,text no. #26,text no. #27,text no. #28,text no. #29,text no. #30,text no. #31,text no. #32,text no. #33,text no. #34,text no. #35,text no. #36))
That it does in fact work without trouble. The problem with the 18-object mapping limit isn't that the forms can't support more than 18 fields internally but that the binding can't support it. However, when I was looking at the ObjectMapping source I noticed that by default the key
of an ObjectMapping is an empty string. And that the field binding are bound with the prefix given and then also with said prefix:
val field1 = f1._2.withPrefix(f1._1).withPrefix(key)
That made me realize that the "top" of the form is just an empty key. For no reason besides rampant curiousity I tried it out with two empty keys since you can see in ObjectMapping 2 that the empty key is used on both fields:
val field1 = f1._2.withPrefix(f1._1).withPrefix(key)
val field2 = f2._2.withPrefix(f2._1).withPrefix(key)
Since the mappings
field in the Mapping
is just a Seq[Mapping]
I figured that underneath it all in the deep merge methods and whatnot that we're not using a map where the keys would conflict, but that they're combined in a non-destructive fashion since they all share this top level key which is how (I believe) play generates your field.nested.thing
mappings based on how you nested the mappings themselves. So, all in all, this means that you can have multiple bindings to the same key (or at the very least to the empty string) and can therefore construct anything larger than 18 fields by breaking it up into smaller components then providing a manual apply
and unapply
method to combine things (as oppose to try to using P36.apply and P36.unapply since those wouldn't work because of tuple limitation I believe)