Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to output JSON in Rails without escaping back slashes

I need to output some JSON for a customer in a somewhat unusual format. My app is written with Rails 5.

Desired JSON:

{
  "key": "\/Date(0000000000000)\/"
}

The timestamp value needs to have a \/ at both the start and end of the string. As far as I can tell, this seems to be a format commonly used in .NET services. I'm stuck trying to get the slashes to output correctly.

I reduced the problem to a vanilla Rails 5 application with a single controller action. All the permutations of escapes I can think of have failed so far.

def index
  render json: {
    a: '\/Date(0000000000000)\/',
    b: "\/Date(0000000000000)\/",
    c: '\\/Date(0000000000000)\\/',
    d: "\\/Date(0000000000000)\\/"
  }
end

Which outputs the following:

{
    "a": "\\/Date(0000000000000)\\/",
    "b": "/Date(0000000000000)/",
    "c": "\\/Date(0000000000000)\\/",
    "d": "\\/Date(0000000000000)\\/"
}

For the sake of discussion, assume that the format cannot be changed since it is controlled by a third party.

I have uploaded a test app to Github to demonstrate the problem. https://github.com/gregawoods/test_app_ignore_me

like image 250
Greg W Avatar asked May 26 '17 17:05

Greg W


3 Answers

Meditate on this:

Ruby treats forward-slashes the same in double-quoted and single-quoted strings.

"/"   # => "/"
'/'   # => "/"

In a double-quoted string "\/" means \ is escaping the following character. Because / doesn't have an escaped equivalent it results in a single forward-slash:

"\/"  # => "/"

In a single-quoted string in all cases but one it means there's a back-slash followed by the literal value of the character. That single case is when you want to represent a backslash itself:

'\/'  # => "\\/"

"\\/" # => "\\/"
'\\/' # => "\\/"

Learning this is one of the most confusing parts about dealing with strings in languages, and this isn't restricted to Ruby, it's something from the early days of programming.

Knowing the above:

require 'json'

puts JSON[{ "key": "\/value\/" }] 
puts JSON[{ "key": '/value/' }]
puts JSON[{ "key": '\/value\/' }]

# >> {"key":"/value/"}
# >> {"key":"/value/"}
# >> {"key":"\\/value\\/"}

you should be able to make more sense of what you're seeing in your results and in the JSON output above.

I think the rules for this were originally created for C, so "Escape sequences in C" might help.

like image 132
the Tin Man Avatar answered Nov 19 '22 23:11

the Tin Man


Hi I think this is the simplest way

.gsub("/",'//').gsub('\/','')

for input {:key=>"\\/Date(0000000000000)\\/"} (printed)

first gsub will do{"key":"\\//Date(0000000000000)\\//"}

second will get you

{"key":"\/Date(0000000000000)\/"}

as you needed

like image 21
Chen Kinnrot Avatar answered Nov 19 '22 23:11

Chen Kinnrot


After some brainstorming with coworkers (thanks @TheZanke), we came upon a solution that works with the native Rails JSON output.

WARNING: This code overrides some core behavior in ActiveSupport. Use at your own risk, and apply judicious unit testing!

We tracked this down to the JSON encoding in ActiveSupport. All strings eventually are encoded via the ActiveSupport::JSON.encode. We needed to find a way to short circuit that logic and simply return the unencoded string.

First we extended the EscapedString#to_json method found here.

module EscapedStringExtension
  def to_json(*)
    if starts_with?('noencode:')
      "\"#{self}\"".gsub('noencode:', '')
    else
      super
    end
  end
end

module ActiveSupport::JSON::Encoding
  class JSONGemEncoder
    class EscapedString
      prepend EscapedStringExtension
    end
  end
end

Then in the controller we add a noencode: flag to the json hash. This tells our version of to_json not to do any additional encoding.

def index
  render json: {
     a: '\/Date(0000000000000)\/',
     b: 'noencode:\/Date(0000000000000)\/',
   }
end

The rendered output shows that b gives us what we want, while a preserves the standard behavior.

$ curl http://localhost:3000/sales/index.json
{"a":"\\/Date(0000000000000)\\/","b":"\/Date(0000000000000)\/"}
like image 11
Greg W Avatar answered Nov 19 '22 23:11

Greg W