1

I have a Model which conforms to Content:

import Vapor
import Fluent

final class User: Model, Content {
    static let schema = "user"
    
    @ID(key: .id)
    var id: UUID?

    @Field(key: "email")
    var email: String

    init() { }

    init(id: UUID? = nil, email: String) {
        self.id = id
        self.email = email
    }
}

This can be fetched from the database and directly returned from a route, so the client receives the users as JSON.

Now I want to add an additional property to the user, which isn't stored in the database but a fixed value or just added after the user is loaded from the database:

final class User: Model, Content {
    // ...
    var someOther: String = "hello"
    // ...
}

or

final class User: Model, Content {
    // ...
    var someOther: String? // set later
    // ...
}

Now the problem is that this property is never added to the JSON, so the client only gets the user's id and email and NOT the someProperty. How do I fix this?

swift-lynx
  • 3,219
  • 3
  • 26
  • 45
  • If the question is "How to exclude properties from Swift 4's Codable?" then the answer is here https://stackoverflow.com/questions/44655562/how-to-exclude-properties-from-swift-4s-codable – Roman Ryzhiy Sep 11 '20 at 10:24
  • My problem is that the `someProperty` is NOT included in the result – swift-lynx Sep 11 '20 at 10:30
  • May we have a look at the piece of code where the coding takes place? – Roman Ryzhiy Sep 11 '20 at 10:32
  • 1
    maybe Fluent by design encode only @Field properties? – imike Sep 11 '20 at 10:32
  • @imike Is it Fluent that does the encoding? OP, maybe you should have a new struct that contains this new field also and then you map to this new struct when sent? – Joakim Danielson Sep 11 '20 at 10:37
  • @JoakimDanielson yeah, but that seems like a hack, I want to know if there is a proper solution – swift-lynx Sep 11 '20 at 10:42
  • Actually I don't see it as a hack, you have one custom type that corresponds to what is persisted and here you need to use the data in a slightly different way so you create a new custom type for this scenario. And you can make this struct immutable also to make the solution even cleaner. – Joakim Danielson Sep 11 '20 at 10:47

1 Answers1

2

When you want to send or receive data from your client that has a different representation from what your model has, it's common practice to define a custom type that you can then convert to and from the model type.

Given this, you would define a new User.Response type that conforms to Content instead of the User model, and return that from your route instead.

extension User {
    struct Response: Content {
        let id: UUID
        let email: String
        let otherValue = "Hello World"
    }
}


func user(request: Request) throws -> EventLoopFuture<User.Response> {
    return User.find(UUID(), on: request.db).flatMapThrowing { user in
        guard let id = user.id else {
           throw Abort(.internalServerError, "User is missing ID")
        }

        return User.Response(id: id, email: user.email) 
    }
}

The reason the additional value that you added wasn't encoded from your model is because the default encoder for model types iterates over the field properties and encodes those properties, so if you have a property that doesn't use one of the ID, Field, Parent, Children, or other field property wrappers, it won't be encoded.

Caleb Kleveter
  • 11,170
  • 8
  • 62
  • 92