13

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

Greg W
  • 5,219
  • 1
  • 27
  • 33
  • Their request shows a lack of understanding how interpreted strings work with escaped characters and your experiment shows you are learning about it. The output you are getting is correct. `"/value/"` is the same as `"\/value\/"` because Ruby sees `"\/"` as an escaped forward slash, which doesn't need escaping, so it removes the backslash. `"\\"` is how we create a backslash in a double-quoted string. In a single-quoted string it'd be `'\'`. – the Tin Man May 26 '17 at 17:46
  • Yeah, I tend to agree there, I'm just trying to meet the requirement without rocking the boat. I might try rendering `/value/` to see if their parser will accept it. As a side note, the following answer mentioning datetimes makes me think it might be a .NET thing: https://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped/1580682#1580682 – Greg W May 26 '17 at 17:52

3 Answers3

11

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)\/"}
Greg W
  • 5,219
  • 1
  • 27
  • 33
  • why not just do a `render text: my_method(string)` and do all this funkiness that you have inside of that method instead of overriding ActiveSupport methods? – WattsInABox May 31 '17 at 17:15
7

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.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • 1
    Thanks for the advice. I'm familiar with the concept of escaping - It's this specific use case that has me stumped. – Greg W May 26 '17 at 18:03
  • 3
    Well, if you're generating the value in your code, simply use single-quotes to define the literal and it'll turn out correct. If the customer doesn't understand you'll have to point them to the documentation. Or charge them extra for tutoring. – the Tin Man May 26 '17 at 18:04
1

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

Chen Kinnrot
  • 20,609
  • 17
  • 79
  • 141