This one has bitten me twice now so I'm writing about it hoping I won't need to discover the solution the hard way again.

You'll see examples of JSON API implementations on top of Rails in the wild all over the place. Demonstrations will show how to test them interactively with cURL and they just apparently work. But somehow the specs (or Cucumber steps in my case last night) aren't passing. When a binding.pry is put in it can be observed that the params has a single key with JSON and a nil value. The CONTENTTYPE is application/x-www-form-urlencoded. All of this even when using the excellent jsonspec gem!

What was missing was that I was using the post helper with string data instead of a hash. Let me illustrate.

# features/api/authentication.feature  
Scenario: Authenticate  
  Given a user account exists identified by email "a_user@example.com" and password "a_password"
  And I post JSON to the authenticate API endpoint with:
  """
  {"user": {
      "email":"a_user@example.com",
      "password":"a_password"
    }}
  """
  Then the JSON at "success" should be "true"

If your customers are other developers its perfectly acceptable to behavior test an API in this way. Moving on...

Here's the relevant step for the "And I post JSON...":

# features/steps/authentication_steps.rb  
⋮
Given /^I post JSON to the (.*?) API endpoint with:$/ do |path, string|  
  post api_path_to(path), string
end  
⋮

That won't work! Its because transmitting a string for the second parameter of post causes the CONTENT_TYPE of the request to be application/x-www-form-urlencoded. The correct implementation is to use a hash for the second parameter, like so:

# features/steps/authentication_steps.rb  
⋮
Given /^I post JSON to the (.*?) API endpoint with:$/ do |path, string|  
  json = JSON.parse(string)
  post api_path_to(path), json # sweet, sweet hashness
end  
⋮

Ah victory.

Still working on sorting out the "Then the JSON at ..." step however.

Update

The other sane approach to this - and not partially compatible with the first approach - is:

# features/api/authentication.feature  
Scenario: Authenticate  
  Given a user account exists identified by email "a_user@example.com" and password "a_password"
  And I send and accept JSON
  And I post JSON to the authenticate API endpoint with:
  """
  {"user": {
      "email":"a_user@example.com",
      "password":"a_password"
    }}
  """
  Then the JSON at "success" should be "true"

The "And I send and accept JSON" sets the stage for... sending and accepting JSON!

# features/step_definitions/api_steps.rb  
Given /^I send and accept JSON$/ do  
  header 'Accept', 'application/json, text/javascript, *'
  header 'Content-Type', 'application/json'
end  

And now we can post the raw string rather than the parsed hash:

# features/step_definitions/authentication_steps.rb  
Given /^I post JSON to the (.*?) API endpoint with:$/ do |path, string|  
  post api_path_to(path), string
end  

Now onto solving "Then the JSON at..." step. This I had a bit of difficulty with because I was trying to use Capybara to test the API but I was mixing it with the post helper method which has absolutely no relationship with Capybara at all. No wonder I was receiving blank pages! And even though json_spec is demonstrated using Capybara that's not going to get you very far. So instead of:

# from json_spec article  
def last_json  
  page.source
end  

I instead use:

# features/support/json_spec_env.rb  
require "json_spec/cucumber"

def last_json  
  # don't mix Capybara and Rack::Test for testing JSON APIs!
  last_response.body
end  

And this lets me use json_spec's excellent matchers for verifying my JSON.

The apipathto method is a convenience helper method for an indirection layer to map human readable resource names to their correct API endpoint:

# features/support/paths.rb  
module NavigationHelpers  
⋮
  def api_path_to(endpoint_name, auth_token = nil)
    case endpoint_name
      when /authenticate/
        path = '/api/sessions/authenticate'
      when /resource/
        path = '/api/resources'
      when /new user/
        path = '/api/users'
    end
    path += "?auth_token=#{auth_token}" unless auth_token.nil?
    path
  end
⋮
end

World(NavigationHelpers)