Gotcha! Working with JavaScript, JSON, & AJAX in Rails 6

When we set out to bootstrap Nomadic Creative, I knew I could handle the build myself. We weren’t looking to re-invent the internet or anything nutty like that, it was just a very intricate and complicated website. I knew Rails could handle the job, but I really never intended for:

A) Other developers to jump into my codebase
B) There to be a real need for a Mobile App

I knew we’d be bootstrapping for a while, so I did what great developers do… I started building.

Fast forward two years. It’s not just time to take what we’ve learned and craft what the data is inspiring us to build… it’s also time to be considerate. Some aspects of our updated feature-set should really be built as a platform, not just a website.

For the purpose of this discussion

The major difference between a platform and a website from this Architects POV is that with a website I can pretty much count on the fact that any new requests are coming from a front end that I wrote. The ActiveRecord Objects can be tightly coupled with controllers, other objects & related records and od course helpers. While it’s not best practice, or even advisable, it’s totally possible & the user probably wont know the difference. (yes I know anyone at anytime can send any request to any endpoint, but that’s an attack on a website, where it would be expected use on a platform)

With a platform I can’t make assumptions about:
1) Where the data is coming from
2) Who packaged it up
3) That there we’re protections in place to groom the data before sending it

With that in mind, I set out to make a complex Rails App compatable with another complex Rails App without sharing a codebase between the two.

I’ve modeled them seperately to force myself to build as if they were micro-services consuming one another. I’m still the only developer on the project, but this time around I’m preparing for what I know is coming:

There are a lot of ways to pass data around the web, but for me there is only 1 standard: JSON.

Coming from a MSFT & .NET background I used XML most of my early career, but there is no question that JSON is the clear winner in the year 2020.

So what’s all that got to do with JS & AJAX in Rails? Well, if we want to truly de-couple the services I’m building for Nomcre from one another, each service is gonna have to pass data to the others somehow. To make matters worse, they can’t rely on the ActiveRecord objects from their partner apps.

Services that need to stand on their own must be able to function without knowing anything about each other.

Where you may be able to do something liek this in a monolith:

@gymnasium.games.find(2).rules.each{|c|x..y}

In a service based architecture it’s entirely possible that @gynmasium- has no model for rules related to a game. It’s also possible that the GymService may want to ask the GameService for some details when needed. Like: How many players are you expecting for this game?

Why Doesn’t It ‘Just Work’?

Using the ‘out-of-the-box’ methods.

.to_json & .as_json

Neither one worked for me. As I mentioned, I needed to send all the information about a complex object with related entities. There’s a ton of great SO & Blog posts about this. There’s a Rails way to do it, but using the vanilla call with includes just didnt work for me.

.to_json(:include => [:related_1, :related_2], :methods => [:method_1]) 

Here’s a quick breakdown of the behavior of .to_json with includes & methods: more here

It didn’t work for a few reasons but I’m guessing the most common reason readers will face: using ActionText.

class MyComplexModel < ApplicationRecord 
has_rich_text :description

end
more here

The issue is clear very quickly when you try to create a JSON object (or string) out of model that uses the has_rich_text decorator. The JSON comes out as expected, but it doesnt include your :description ‘field’. Why not? Well, your :description field isn’t actually on the MyComplexModel DB Table.

Is there a solution? Of course! They’re called Serializers and I’ll cover them in a bit.

Gotcha #1

JSON shows up differently on the screen depending on where you’re viewing it, and how it was printed on screen.

If you’re using the rails console

@model.to_json 
#=> "{\"id\":1,\"title\":\"this is a title \",\"hero\":\"hero henry \",\"created_at\":\"2020-02-05T18:10:54.047Z\",\"updated_at\":\"2020-02-05T18:14:38.190Z\"}"

but that same call in an HTML view will be:

<%= @model.to_json %> shows on the page as:
{“id”:1,”title”:”this is a title “,”hero”:”hero henry “,”created_at”:”2020–02–05T18:10:54.047Z”,”updated_at”:”2020–02–05T18:14:38.190Z”}

notice that in the HTML view, there is no need to show the escape characters, because HTML’s only job is to show you something. So, it interprets the \ as an instruction, and not something to show on screen.

Gotcha #2

Let’s say the title had an apostrophe in it.

this's a title

For the most part, this isn’t going to screw with your JSON because you used well formatted JSON ( calling .to_json)and wrapped your keys & values in double quotes “ ”.

Until you try to send that JSON somewhere else. From the browser, depending on the browser and version, trying to send JSON with an un-escaped single quote can break AJAX.

Even though you know your JSON is well formed, you can’t know what all your users will try to do, liek be gramatically correct. Before we send the JSON anywhere we have to use the JavaScript Helper to make sure we’re preserving the text as intended without giving the browser or JS interpreter a chance to fuck it up. JSON with unescaped quotes will prevent the button click from firing the method!

<script type="text/javascript">
$(document).ready(function() {
$("#button_1").click(function(e) {
e.preventDefault();
$.ajax({
url: '<%= reciever_path %>',
method: "POST",
dataType: 'json',
data: {"model_as_json":'<%=raw escape_javascript @json %>'},
success: function(result) {
alert('ok');
},
error: function(result) {
alert('error');
}
});
})
});
</script>

Gotcha #3: .to_json vs .as_json

In the Rails world we get accustomed to seeing the fat arrow => for a lot of things especially assigning some type of value.

.to_json(:include => [:related_1, :related_2])

Admittedly this one is more personal, but when I started using the _json methods I expected the result to be a JSON object, not a string. Then when I saw the output, it looked better to my eyeballs.

s.to_json
=> "{\"id\":1,\"title\":\"this is a title\",\"hero\":\"hero henry \",\"created_at\":\"2020-02-05T18:10:54.047Z\",\"updated_at\":\"2020-02-13T19:00:27.743Z\"}"

s.as_json
=> {“id”=>1, “title”=>”this is a title”, “hero”=>”hero henry “, “created_at”=>”2020–02–05T18:10:54.047Z”, “updated_at”=>”2020–02–13T19:00:27.743Z”}

Skipping the escape characters, and adding a little space with fat arrows (=>) instead of colons ( : ) felt much more readable to me in the console.

And at first glance, as_json returns a JSON object. Notice the output of .as_json() has no quotes around the outside of the opening and closing braces {} .

Unfortuantely JavaScript & JSON dont really like the fat arrow much at all. So, using the resulting .as_json() object version for me wasn’t an advantage over turning it into a string in the end.

Now What?

There are actually a 2 questions with the same answer addressed here:

1- How do I get the right fields & relationships into JSON?
I need to be including the ActionText relationships. Also, how can I make the related object contain ther right information so that I’m not sending up a ton of duplicate data.

2- How can I ensure the hash can be consumed by a different application (or service)?
In the vanillla flavor of MVC Rails if I want to create a new chapter in book I could just follow the pattern of

def new
set_book
@chapter = Chapter.new
end
def create
set_book
@chapter = Chapter.new(chapter_params)
...
end

and let the chapter _form.html.erb helper zip up my request into a nice and neat little package.

But what about now that I have this massive JSON tree that has contaxtual data not a 1:1 realtionship between the key/value pairs and the models I want to use in service 2?

The Answer: Custom Serializers

Custom Serializers let us change any models default behavior for the methods:

.to_json()
.as_json()

True to form using The Rails Way there is a very straight forward convention for telling the application what we really want it to do when either of the methods above are called.

All we have to do is create a folder for

app/services 

and place a file in it with the naming convention of

<name_of_model>_serializer.rb

& fill it with a class derived from ActiveModel::Serializer

class MyComplexObjectSerializer < ActiveModel::Serializer 
...
end

or alternatively you can use the Rails generator

> rails g serializer my_complex_object 

Now that we have a custom serializer, we can tell it what fields we want to include and methods we want it to execute whenever we call:

@myComplexObject.to_json() or .as_json() 

A serializer class can be as simple as

class MyComplexObjectSerializer < ActiveModel::Serializer 
attributes :id, :title, :hero
end

But, as I mentioned I have a couple of qirky needs.

First, I need to include the ActionText field :description in the JSON output. If I simply update the serializer class to include :description after :hero that should be the end of it, but since ActionText is a wrapper around a relationship it’s not so simple.

In my particular case what I really wanted was a plain text representation of whats being stored in the ActionText :description field.

To make my app work the way I want it to, I’m going to have to make the Serializer run at least one method. Let’s call that method :rich_description and update the serializer class accordingly:

class MyComplexObjectSerializer < ActiveModel::Serializer 
attributes :id, :title, :hero, :rich_description

def rich_description
@object.description.as_plain_text
end
end

Luckily for us the ActionText wrapper gives us a nice little method to get the content as plain text even if there are attachements, links, etc…

Now, when the serializer gets called through a .to_json() or .as_json() call, instead of trying to add the trix editor record (ActionText saves a TrixEditor record) through the call to include :description in the serialization, it will run the method rich_description and include the output of that method in the hash. In this case the output is simply a string.

So I finally got MyComplexObject to give me all the info about itself, but what about when I try to include the JSON for their relationships?

If MyComplexObject is cool with just printing all the fields of its related MySimpleObject1 then I can just use the vanilla @mySimpleObject1.to_json() by simply telling the Complex Object’s serializer about the relationship.

class MyComplexObjectSerializer < ActiveModel::Serializer 
attributes :id, :title, :hero, :rich_description
has_many :my_simple_object1s def rich_description
@object.description.as_plain_text
end
end

But if the class MySimpleObject1 also uses an ActionText field?

You guessed it! A Custom Seriealizer to the rescue!

You can go ahead and create the new Custom Serializer the same way using the generator. Once it’s saved, we can add it to the MyComplexObjectSerializer so that it knows — Hey dude, everytime you try to represent this relationship with MySimpleObject1 in JSON… please use the Serializer I made for that model, actaully please do the same for each instance of the simple object that you find.

class MyComplexObjectSerializer < ActiveModel::Serializer 
attributes :id, :title, :hero, :rich_description
has_many :my_simple_object1s,
each_serializer: MySimpleObject1Serializer
def rich_description
@object.description.as_plain_text
end
end

Now, we’re cookin’ with grease!

This post was much longer than I expected when I set out to write it… so I’ll update post with the answer to #2 above in a few days. I really gotta get back to work. :)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store