Play Framework Form only 18 params
Asked Answered
C

3

6

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.

Criminal answered 14/6, 2012 at 6:25 Comment(1)
The best way is to get rid of Play Forms and parse Json directly. It was so easy that I deeply regret all the time lost fighting against the 22 fields absurdity.Tanguay
A
3

I opened a ticket on this issue a couple of weeks ago.

If you vote for it, perhaps it will get a look from Play devs.

Doubt it's high on their priority list (unfortunate given that it's more or less just a copy-paste to tack on the 19, 20, 21, and 22 Mapping[T])

If you are desperate, you could fork Play; otherwise, come up with a workaround, for example, utilizing nested forms or splitting up > 22 field model into separate forms.

Ankle answered 14/6, 2012 at 6:48 Comment(4)
You both had good very answers, but I gave the accepted answer to Erik Bakker as he answered first.Criminal
If I have time, I'll try to add ObjectMapping upp to 22 and do a pull request against Play20. The information in Erik's answer should be enough to get it working. Mostly copy/paste. Won't solve my case but makes more sense to have the same as Scala Case classes. Edit: Actually this might not work. The signatures needs to take apply, unapply arguments as well. So then we're already at 20 arguments.Criminal
@Criminal actually, I answered first, not to mention taking the initiative via Lighthouse ticket a couple of weeks ago ;-) Anyway, Scala itself is the culprit here (22 tuple limit), not necessarily Play (they just elected to, for some reason, stop at 18). Long & short, with 29 fields you'll need to break out of the Play box, so to speak, and come up with your own solutionAnkle
that is true, by 2 min. My browser said otherwise (?) when I looked first. I have set your answer as the correct answer, in the name of fairness ;). I have to say you both got equally good answers.Criminal
M
4

The mapping method you use is not a single method, but it is overloaded. For a single parameter, it has two type parameters, one for the result type and one for the element you're consuming. It constructs an ObjectMapping1. For two parameters, it has three type parameters and it constructs an ObjectMapping2.

These ObjectMappingX classes are defined up to ObjectMapping18, as you've noticed. You can find it in Play's source code in play/api/data/Forms.scala

The recommended solution is to avoid non-nested forms of this size. If that is unavoidable, you can either use a different library than the built-in Play one, or you can define the missing ObjectMappingX objects and corresponding methods to construct them yourself.

Marcin answered 14/6, 2012 at 6:50 Comment(1)
As I need 29 parameters, I'm going to look into if I can do a preprocessing step before I do form.bindFromRequest(request). I plan to convert param1 param2 to group.param1, group.param2 so I can have nested case classes in the form mapping.Criminal
A
3

I opened a ticket on this issue a couple of weeks ago.

If you vote for it, perhaps it will get a look from Play devs.

Doubt it's high on their priority list (unfortunate given that it's more or less just a copy-paste to tack on the 19, 20, 21, and 22 Mapping[T])

If you are desperate, you could fork Play; otherwise, come up with a workaround, for example, utilizing nested forms or splitting up > 22 field model into separate forms.

Ankle answered 14/6, 2012 at 6:48 Comment(4)
You both had good very answers, but I gave the accepted answer to Erik Bakker as he answered first.Criminal
If I have time, I'll try to add ObjectMapping upp to 22 and do a pull request against Play20. The information in Erik's answer should be enough to get it working. Mostly copy/paste. Won't solve my case but makes more sense to have the same as Scala Case classes. Edit: Actually this might not work. The signatures needs to take apply, unapply arguments as well. So then we're already at 20 arguments.Criminal
@Criminal actually, I answered first, not to mention taking the initiative via Lighthouse ticket a couple of weeks ago ;-) Anyway, Scala itself is the culprit here (22 tuple limit), not necessarily Play (they just elected to, for some reason, stop at 18). Long & short, with 29 fields you'll need to break out of the Play box, so to speak, and come up with your own solutionAnkle
that is true, by 2 min. My browser said otherwise (?) when I looked first. I have set your answer as the correct answer, in the name of fairness ;). I have to say you both got equally good answers.Criminal
H
2

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)

Hypothermal answered 13/9, 2017 at 14:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.