Search code examples
iosswift5nsjsonserializationjsonencoder

Another Invalid top-level type in JSON write crash


So, Xcode 13.1, iOS 15.0, I am stuck with this JSON serialization for a rather important project which has to be delivered yesterday(!) as usual, and instead crashes.

This is the piece of code I'm dealing with:

var jsonData: Data
    do {
        jsonData = try JSONSerialization.data(withJSONObject: roomBookings, options: [])
    } catch {
        print("error: ", error)
        return false
    }

This is the object I am trying to JSON serialize:

Rooms.RoomBookings(
bookings: [Rooms.RoomBooking(
bookingID: "23EB86CB-A918-47D4-ADDB-346DBB4E3471",
 roomID: "BgX86SbN0UifijkwU8HZ", 
isAllDay: false, 
startTimestamp: 1636440856861, 
endTimestamp: 1636444456861, 
user: "trialUser")
])

This is the error I keep getting, which makes the app crash (of course we can avoid the actual crash checking with isValidJSONObject, but that's not the point here).

Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: '*** +[NSJSONSerialization dataWithJSONObject:options:error:]:
 Invalid top-level type in JSON write'

Here below is the model, The error is focused on the top-level type, what's wrong with that? It's just an Array of RoooBooking instances, conformed with Codable, so what?

Someone knows what am I doing wrong? That would be greatly appreciated!

// MARK: - RoomBookings
struct RoomBookings: Codable {
    var bookings: [RoomBooking]

    enum CodingKeys: String, CodingKey {
        case bookings = "bookings"
    }
}

// MARK: RoomBookings convenience initializers and mutators
extension RoomBookings {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(RoomBookings.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        bookings: [RoomBooking]? = nil
    ) -> RoomBookings {
        return RoomBookings(
            bookings: bookings ?? self.bookings
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - RoomBooking
struct RoomBooking: Codable {
    var bookingID: String
    var roomID: String
    var isAllDay: Bool
    var startTimestamp: Int
    var endTimestamp: Int
    var user: String

    enum CodingKeys: String, CodingKey {
        case bookingID = "bookingId"
        case roomID = "roomId"
        case isAllDay = "isAllDay"
        case startTimestamp = "startTimestamp"
        case endTimestamp = "endTimestamp"
        case user = "user"
    }
}

// MARK: RoomBooking convenience initializers and mutators

extension RoomBooking {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(RoomBooking.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        bookingID: String? = nil,
        roomID: String? = nil,
        isAllDay: Bool? = nil,
        startTimestamp: Int? = nil,
        endTimestamp: Int? = nil,
        user: String? = nil
    ) -> RoomBooking {
        return RoomBooking(
            bookingID: bookingID ?? self.bookingID,
            roomID: roomID ?? self.roomID,
            isAllDay: isAllDay ?? self.isAllDay,
            startTimestamp: startTimestamp ?? self.startTimestamp,
            endTimestamp: endTimestamp ?? self.endTimestamp,
            user: user ?? self.user
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

// MARK: - Helper functions for creating encoders and decoders

func newJSONDecoder() -> JSONDecoder {
    let decoder = JSONDecoder()
    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
        decoder.dateDecodingStrategy = .iso8601
    }
    return decoder
}

func newJSONEncoder() -> JSONEncoder {
    let encoder = JSONEncoder()
    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
        encoder.dateEncodingStrategy = .iso8601
    }
    return encoder
}

Solution

  • Try

        var jsonData: Data
        do {
            jsonData = try JSONEncoder().encode(roomBookings)
            print(String(data: data, encoding: .utf8)!) //to check the actual O/P am adding it here, remove it from your code
        } catch {
            print("error: ", error)
            return false
        }
    

    O/P:

    { "bookings": [{ "endTimestamp": 1, "roomId": "1", "user": "abcd", "isAllDay": true, "bookingId": "1", "startTimestamp": 1 }] }