Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift JSONDecoder can't decode valid JSON with escape characters

In playgrounds, the following code produces an error:

import Foundation

struct Model: Codable {

  let textBody: String

  enum CodingKeys: String, CodingKey {
    case textBody = "TextBody"
  }
}

let json = """
          {
            "TextBody": "First Line\n\nLastLine"
          }
          """.data(using: .utf8)!


let model = try! JSONDecoder().decode(Model.self, from: json)

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Unescaped control character around character 27." UserInfo={NSDebugDescription=Unescaped control character around character 27.}))): file MyPlayground.playground, line 19

The JSON above is perfectly valid according to JSONLint. So what gives?

Update:

I need a solution that will handle data returned from an API. Here is something I came up with so far, but it's gross...

if let data = data,
        let dataStr = String(data: data, encoding: .utf8),
        let cleanData = dataStr.replacingOccurrences(of: "\n", with: "", options: .regularExpression).data(using: .utf8)
      {

        do {

          let response = try JSONDecoder().decode(T.Response.self, from: cleanData)

          completion(.success(response))

        } catch (let error) {

          print(error.localizedDescription)

          completion(.failure(ApiError.decoding))

        }

      }
like image 869
Brandt Avatar asked Sep 04 '19 23:09

Brandt


1 Answers

Your json in your playground is represented incorrectly. It is constructed from a string literal with the \n in it. But that is being replaced newline characters in your string before it’s converted to a Data. But a newline is not permitted within JSON string. You need the two separate characters, namely \ followed by n in your string within the JSON. You can do that by escaping the \ with another \, e.g.:

let json = """
    {
        "TextBody": "First Line\\n\\nLastLine"
    }
    """.data(using: .utf8)!

Or, alternatively, in Swift 5 and later, you can use extended string delimiters, such as:

let json = #"""
    {
        "TextBody": "First Line\n\nLastLine"
    }
    """#.data(using: .utf8)!

Or:

let json = #"{"TextBody": "First Line\n\nLastLine"}"#
    .data(using: .utf8)!

I’d be very surprised if your web service was returning a JSON with newline characters (0x0a) within its string values, rather than \ character followed by n character. That would only happen if some inexperienced back-end developer was manually building JSON rather than using functions that do this properly.

You say that you’re seeing \n in Postman. That suggests that your server response is correct, that there are two characters, \ followed by n, within the string. For example, here is a web service that echoed my input back and this JSON is well formed with \ followed by n:

enter image description here

If your output looks like the above, then your JSON is valid and the problem in your code snippet above is merely a manifestation of how you represented this JSON in a string literal in Swift code in your playground.

You only need to worry if you see "First line on one line in this Postman “raw” view and see Lastline" on the next line (presumably with no \n).

Bottom line, we should ignore the error in your playground. Parse your actual server response (not cutting-and-copying the JSON into code, or at least not without those extended string literals). Focus on what errors, if any, you get when you parse the actual server response. I wager that if you run your parser on your actual server response, you won’t get this “Unescaped control character” error.

like image 151
Rob Avatar answered Sep 21 '22 03:09

Rob