Vapor: Date object is decoded in a string format, but the decoder expects a double

Viewed 514

I have a data model that looks like this:

struct Post: Content, MySQLModel, Migration, Equatable {
    var id: Int?
    var userId: Int
    var title: String
    var body: String
    var creationDate: Date?
    var lastEditDate: Date?

    static func prepare(on connection: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.create(self, on: connection) { builder in
            builder.field(for: \.id, isIdentifier: true)
            builder.field(for: \.userId)
            builder.field(for: \.title)
            builder.field(for: \.body, type: .text())
            builder.field(for: \.creationDate)
            builder.field(for: \.lastEditDate)
        }
    }
}

And if I have some instances in the database, I can safely make a query passing the id of the post in the path, and I would get an object with a creation/last edit date formatted in a string format:

func retrievePost(on req: Request) throws -> Future<Post> {
    let id = try req.parameters.next(Int.self)

    return Post.find(id, on: req).map(to: Post.self) { post in
        guard let post = post else {
            throw Abort(.notFound)
        }

        return post
    }
}

If I make a GET query, this is what I get back in the response body:

{
    "body": "value",
    "id": 8723,
    "title": "value",
    "creationDate": "2020-05-27T15:24:41Z",
    "userId": 0
}

And this is my PUT method implementation:

func updatePost(on req: Request) throws -> Future<Post> {
    var updatedPost = try req.content.syncDecode(Post.self)

    guard let id = updatedPost.id else {
        throw Abort(.badRequest)
    }

    return Post.find(id, on: req).flatMap { post in
        guard let _ = post else {
            throw Abort(.notFound)
        }

        return updatedPost.save(on: req)
    }
}

But if I send a PUT request passing the same exact fields that I got in the GET response body, with the creation date formatted as a string, I get this error:

{
    "error": true,
    "reason": "Could not convert to `Double`: str(\"2020-05-27T15:24:41Z\")"
}

It's expecting a double. If I try sending the number of seconds after 1970 it works, but I don't understand why the same date object is encoded using a string date and decoded using a double. How to go around this problem?

2 Answers

Using Vapor 4, I specified a custom JSONDecoder. This can be done globally, or as shown here for a single request.

In this scenario, I was decoding a POST'ed struct GetLogsRequest that included a date field.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let request = try req.content.decode(GetLogsRequest.self, using: decoder)

See https://docs.vapor.codes/4.0/content/

Exactly like you, I use:

struct MyForm {
    let myDate:Date
}

In the .leaf form, I use a different field name to allow the user to modify the date:

<input type="date" name="userDate" value="#date(myDate, "yyyy-MM-dd")">

Then, in the submit button's onclick event, I use javascript to calculate the timestamp value for the date and this gets returned in the 'original' field:

var d = new Date(f["userDate"].value);
f['myDate'].value = d.getTime()/1000;
return true;

The decode that is giving you trouble should now work okay.

I also use a customTag to represent the date in a shorter format:

struct DateTag:LeafTag
{
    public func render(_ context:LeafContext) throws -> LeafData
    {
        try context.requireParameterCount(2)
        guard let timestamp = context.parameters.first?.double else { return .string("") }
        guard let format = context.parameters[1].string else { throw "DateTag needs a format to work on" }
        let df = DateFormatter()
        df.dateFormat = format
        return .string(df.string(from:Date(timeIntervalSince1970:timestamp)))
    }
}

The nested double-quotes are okay as Leaf only works on the inner set, leaving the outer set for the browser.

Related