Ask Ben: Passing ColdFusion Structs And Arrays In The URL
I was wondering is there any way to pass an entire structure or array via a url parameter? I think I remember reading that you can but when I try to pass all form fields like this http://www.mysite.com?MyForm=#Form# (I understand that #form# would be the entire form scope structure) it does not work. Am I doing something wrong?
URL values are just strings; they may represent dates and numbers and strings, but the values are actually just strings. As such, you cannot pass ColdFusion structs and arrays in the URL as they are complex values and ColdFusion does not have a built-in way to convert them to simple (string) values. But, of course, there are ways to manually pass ColdFusion structs and arrays in the URL. We have a few options:
We can loop over the struct and add each key-value pair as a name-value pair to the URL query string.
We can serialize the struct / array and add it to the URL query string as single name-value pair.
The first option, doesn't really work for two reasons: One, we might have keys within the structure that conflict with query string parameters already in the url. And two, what happens if the structure has nested structures or arrays - how do we represent that.
Our best bet here is serialize the ColdFusion struct or array for the URL. This will keep all of the struct data contained to a single URL variable and it will inherently take care of any nested arrays or structures.
When passing the ColdFusion struct or array in the URL, we are going to have to serialize it in WDDX data. In order to make this as simple as possible, we are going to wrap this functionality up into a ColdFusion user defined function, SerializeURLData():
<cffunction name="SerializeURLData" access="public" returntype="string" output="false" hint="Serializes the given data using WDDX. Optionally encodes for URL."> <!--- Define arguments. ---> <cfargument name="Data" type="any" required="true" hint="ColdFusion struct or array data." /> <cfargument name="Encode" type="boolean" required="false" default="true" hint="Flag for URL encoded format." /> <!--- Create local scope. ---> <cfset var LOCAL = StructNew() /> <!--- Serialize the data using WDDX. This will convert the ColdFusion data into WDDX standards XML data. ---> <cfwddx action="CFML2WDDX" input="#ARGUMENTS.Data#" output="LOCAL.WDDXData" usetimezoneinfo="false" /> <!--- Check to see if we are encoding the data for URL. If do this here, then the user has to be carful NOT to run URLEncodedFormat() on the returned data (that would be like double-escaping it). ---> <cfif ARGUMENTS.Encode> <!--- Return the encoded data. ---> <cfreturn URLEncodedFormat( LOCAL.WDDXData ) /> <cfelse> <!--- Return the data as-is. ---> <cfreturn LOCAL.WDDXData /> </cfif> </cffunction>
This function takes two arguments: the data and a flag for encoding. By default, the WDDX data will be URL encoded before it is returned. This is important to understand because it means you should NOT manually encode the URL after this data has been included. Doing so would lead to doubly-escaped URL characters which will not decode as expected. Also realize that since we are encapsulating the serialization / deserialization method, we could easily swap this out with a JSON implementation (encapsulation is a wonderful thing).
Once this data is passed to a page via the URL (of FORM scope), we are going to need a way to deserialize it. In order to make this as simple as possible we are going to wrap this functionality up into a ColdFusion user defined function, DeserializeURLData():
<cffunction name="DeserializeURLData" access="public" returntype="any" output="false" hint="Converts the URL WDDX data back into ColdFusion data objects."> <!--- Define arguments. ---> <cfargument name="Data" type="string" required="true" hint="WDDX data (can be URL encoded)." /> <!--- Define the local scope. ---> <cfset var LOCAL = StructNew() /> <!--- When it comes to converting the data from WDDX back into ColdFusion, we have to make sure that it is not URL encoded. If it is NOT URL encoded, then our first character will be "<". If not, then the data is URL encoded and we must first decode it. ---> <cfif (Left( ARGUMENTS.Data, 1 ) NEQ "<")> <!--- Decode the data. ---> <cfset ARGUMENTS.Data = URLDecode( ARGUMENTS.Data ) /> </cfif> <!--- ASSERT: At this point, no matter how the data was passed to us, it is not in true WDDX format. ---> <!--- Convert the WDDX back to ColdFusion. ---> <cfwddx action="WDDX2CFML" input="#ARGUMENTS.Data#" output="LOCAL.Data" /> <!--- Return the ColdFusion data. ---> <cfreturn LOCAL.Data /> </cffunction>
Notice that this data will check to see if the WDDX data is URL encoded. If it is, the UDF will decode the passed in data (back into WDDX) before it deserializes it back into ColdFusion data.
Ok, so let's see this in action. To demo this URL-passing functionality, we are going to create a simple page that passes a serialized struct back to itself via the URL. On the first load of the page, we will create a struct, dump it out, and provide the link. Once the link has been clicked, will deserialize the data and dump it out.
<!--- Kill extra output. ---> <cfsilent> <!--- Param the URL values. ---> <cfparam name="URL.actress" type="string" default="" /> </cfsilent> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>Passing ColdFusion Structs In URL</title> </head> <body> <!--- Check to see if we have our URL data. ---> <cfif Len( URL.actress )> <!--- We have the URL data. Grab the actress data our of the URL and deserialize it so that we can use it in ColdFusion. ---> <cfset objActress = DeserializeURLData( URL.actress ) /> <h4> Recieved Struct: </h4> <!--- Dump out the struct so see can see what we just grabbed out the URL. ---> <cfdump var="#objActress#" label="Actress From URL" /> <cfelse> <!--- We don't have anything in our URL just yet. Let's build a struct which we will then pass through the URL in a link back to this page. ---> <cfset objActress = StructNew() /> <!--- Populate the struct with some data. We are gonig to intentionally include some complex data types to demonstrate that this can handle intricate data sets. ---> <cfset objActress.Name = "Maria Bello" /> <cfset objActress.Birthday = "04/18/1967" /> <cfset objActress.HomeTown = "Norristown, PA" /> <!--- Create an array for some of her movies. ---> <cfset arrMovies = ArrayNew( 1 ) /> <cfset arrMovies[ 1 ] = "A History of Violence" /> <cfset arrMovies[ 2 ] = "Thank You for Smoking" /> <cfset arrMovies[ 3 ] = "The Cooler" /> <cfset arrMovies[ 4 ] = "Coyote Ugly" /> <!--- Store the movies in the actress. ---> <cfset objActress.Movies = arrMovies /> <h4> Pass Struct: </h4> <!--- Dump out the struct so see can see what we are about to pass via the URL. ---> <cfdump var="#objActress#" label="Actress To Be Passed" /> <!--- Create the URL. ---> <cfset strURL = ( CGI.script_name & "?actress=" & SerializeURLData( objActress ) ) /> <p> <!--- Output the link. ---> <a href="#strURL#">Pass the Struct!</a> </p> </cfif> </body> </html>
When we run the page the first time, we get this CFDump output:
... and when we click the link and deserialized the URL-passed struct, we get this CFDump output:
This seems to work nicely. The one issue that we might need to consider with this technique is that the URL has potential to become very long. WDDX is not exactly a space-efficient serialization technique. The XML standard that is uses is quite verbose and when it is URL encoded, it becomes even longer. Using JSON would cut down on this tremendously.
Want to use code from this post? Check out the license.
I think your last point about character length of the encoded serialized content is a very important one. Most web servers carry limitations on how long the url string can be (I believe the two standard sizes are 255 and 1024). This might be an adjustable value but to play it safe when you submit a lengthy amount of data, send it in a POST request.
Wouldnt it just make more sense to assign the variable to a persistent scope and pass it that way? In 10 years of ColdFusion development, I have never come across a situation that would require passing that much data in the URL.
Not to say I havent done it a time or two in the past ;o) But hindsight being 20/20 I wouldnt do it again. Persistent scopes are your friend!
Both of you are correct. This probably part of the reason that this functionality is not built into coldfusion already :) (well JSON will be part soon, but again, not what was intended).
When I answer these questions, I have very little data to go off of, so I do the best I can. I think it's great that you guys bring up the "devils advocate" positions as I think it forces us to think not just about answers, but about if they are "correct". I should be doing more of that - not just answering, but "leading" people down the right path.
@Ben - I think you are doing a great job and already are leading people down the right path. You've put out a multitude of really interesting and really informative posts (like this one), that have helped improve our community and teach people like myself a thing or two.
@Russ - Don't forget, he could have been passing this data along to some web service (RESTfull perhaps) that wouldn't have access to his shared scope.
Your doing a great job and a great service to the community! I have talked to several developers over the last few weeks that were Junior level developers and more than one of them mentioned your blog and how much they liked your approach to learning and relaying your lessons learned! Keep the the good work! I just wish I had the drive to post to my blog as much as you do!
Remember, its not always about if you can do it, rather than if you 'should' do it.
Very good point! Didnt really consider that.
Thanks fellas. I will be sure to keep up the hard work. I am more than happy to help people and I totally pumped when I actually find out that I DO help people :D
Hey Ben for this user example could be used get method instead post right? ;-) Form structure will be passed along URL querystring.
Yes, the form variables will passed via the URL in a Form GET, but the that can still only pass along simple values, not complex values.
Good article, but what if you had a huge query (or structure) that would exceed the maximum length of the URL? From what I understand some browsers have a limit to the length a URL can be.
One way to get around this would be to store your query (or structure) in the session scope and label the variable name with some kind of uniqueID. then pass that unique ID in the URL and retrieve the query (or structure) from the session scope using the passed uniqueID.
Page1.cfm contains a query variable named myResults
Page1.cfm stores this query in the session scope called session.myResults_12345
Page1.cfm has a link that takes the user to Page2.cfm?id=12345
Page2.cfm looks at the session scope and retrieves the variable: session.myResults_#ID#
This should accomplish the same thing, right?
Yes, browsers do have URL size restrictions. Furthermore, I would NOT suggest passing arrays and structs via the URL in general, this was more to show that it could be done and to answer a question. If you have large objects that need to persist from page to page, storing them in the SESSION is probably the best way to go.
Once again Ben... you save the day :D
I know this is a really old post, and I hope you still get notifications for this comment.
Anyway, so in CF8 I can use serializeJSON on a struct, but I still can't pass the resulting via the URL.Would I still need to parse through the serialized data and make key-value pairs?
I like D Levins strategy. put it in the session scope
session.str_obj = structNew();
session.str_obj.item_1 = "";
but how can you populate this struct from a query?