servant-swagger icon indicating copy to clipboard operation
servant-swagger copied to clipboard

Modelling sum types

Open commandodev opened this issue 7 years ago • 10 comments

How do you go about expressing a sum type like this:

data WithdrawlResult =
    WithdrawlError ClientError   -- ^ Error with http client error
  | WithdrawlSuccess Transaction -- ^ Success with transaction details
  deriving (Show, Typeable, Generic)

instance ToJSON WithdrawlResult where
    toJSON (WithdrawlSuccess txn) =
        object ["success" .= txn]
    toJSON (WithdrawlError err) =
        object ["error" .= show err]

wdDesc :: Text
wdDesc = "An object with either a success field containing the transaction or "
      <> "an error field containing the ClientError from the wallet as a string"

instance ToSchema WithdrawlResult where
    declareNamedSchema _ = do
        txnSchema <- declareSchemaRef (Proxy :: Proxy Transaction)
        errSchema <- declareSchemaRef (Proxy :: Proxy String)
        return $ NamedSchema (Just "WithdrawlResult") $ mempty
          & type_ .~ SwaggerObject
          & enum_ ?~ [ object ["success" .= toJSON txnSchema]
                               , object ["error"   .= toJSON errSchema]
                               ]
          & properties .~ (mempty
               & at "success" ?~ txnSchema
               & at "error" ?~ errSchema)
          & description .~ (Just $ wdDesc)

Ideally I'd want to express that I'm expecting a JSON object with either { success: {...}} or { error: "Some message" } I can't quite figure out how to do that...

commandodev avatar Jun 28 '18 09:06 commandodev

Can Swagger2 represent those at all? AFAIK the best you can do is mark both fields optional.

ping @fizruk

phadej avatar Jun 28 '18 10:06 phadej

Ah I see. Thanks @phadej - how do you mark the fields as optional? I also tried doing an explanation, but you can't see wdDesc anywhere in the UI...

commandodev avatar Jun 28 '18 12:06 commandodev

So yeah, true sum types are not supported well by Swagger 2.0.

Note: sum types can be supported in OpenAPI 3.0, via oneOf.

Still, you can do as @phadej suggested — mark both fields optional. Additionally you can specify that there should be exactly 1 field (no more, no less) using minProperties and maxProperties ("properties" is what fields are called in a JSON object).

You can actually see that approach in an existing instance for Either a b:

>>> BSL8.putStrLn . encodePretty $ toSchema (Proxy :: Proxy (Either Integer String))
{
    "minProperties": 1,
    "maxProperties": 1,
    "type": "object",
    "properties": {
        "Left": {
            "type": "integer"
        },
        "Right": {
            "type": "string"
        }
    }
}

If you look at the definition, you'll find that it's actually a derived Generic-based implementation:

instance (ToSchema a, ToSchema b) => ToSchema (Either a b)

That means you can achieve the same for your type. Note that you can use genericDeclareNamedSchema explicitly to specify property names for constructors:

data WithdrawlResult =
    WithdrawlError ClientError   -- ^ Error with http client error
  | WithdrawlSuccess Transaction -- ^ Success with transaction details
  deriving (Show, Typeable, Generic)

wdDesc :: Text
wdDesc = "An object with either a success field containing the transaction or "
      <> "an error field containing the ClientError from the wallet as a string"

instance ToSchema WithdrawlResult where
  declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions
    { constructorTagModifier = map toLower . drop (length "Withdrawl") }
    & mapped.mapped.schema.description ?~ wdDesc

You can check that it works in GHCi:

>>> BSL8.putStrLn . encodePretty $ toSchema (Proxy :: Proxy WithdrawlResult)
{
    "minProperties": 1,
    "maxProperties": 1,
    "type": "object",
    "description": "An object with either a success field containing the transaction or an error field containing the ClientError from the wallet as a string",
    "properties": {
        "error": {
            ...
        },
        "success": {
            ...
        }
    }
}

fizruk avatar Jun 28 '18 13:06 fizruk

Nice! Thanks a lot!

On Thu, 28 Jun 2018 at 14:52, Nickolay Kudasov [email protected] wrote:

So yeah, true sum types are not supported well by Swagger 2.0 (and OpenAPI 3.0 AFAIK).

Still, you can do as @phadej https://github.com/phadej suggested — mark both fields optional. Additionally you can specify that there should be exactly 1 field (no more, no less) using minProperties and maxProperties ("properties" is what fields are called in a JSON object).

You can actually see that approach in an existing instance for Either a b:

BSL8.putStrLn . encodePretty $ toSchema (Proxy :: Proxy (Either Integer String)) { "minProperties": 1, "maxProperties": 1, "type": "object", "properties": { "Left": { "type": "integer" }, "Right": { "type": "string" } } }

If you look at the definition, you'll find that it's actually a derived Generic-based implementation:

instance (ToSchema a, ToSchema b) => ToSchema (Either a b)

That means you can achieve the same for your type. Note that you can use genericDeclareNamedSchema explicitly to specify property names for constructors:

data WithdrawlResult = WithdrawlError ClientError -- ^ Error with http client error | WithdrawlSuccess Transaction -- ^ Success with transaction details deriving (Show, Typeable, Generic) wdDesc :: Text wdDesc = "An object with either a success field containing the transaction or " <> "an error field containing the ClientError from the wallet as a string" instance ToSchema WithdrawlResult where declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions { constructorTagModifier = map toLower . drop (length "Withdrawl") } & mapped.mapped.schema.description ?~ wdDesc

You can check that it works in GHCi:

BSL8.putStrLn . encodePretty $ toSchema (Proxy :: Proxy WithdrawlResult) { "minProperties": 1, "maxProperties": 1, "type": "object", "description": "An object with either a success field containing the transaction or an error field containing the ClientError from the wallet as a string", "properties": { "error": { ... }, "success": { ... } } }

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/haskell-servant/servant-swagger/issues/80#issuecomment-401042442, or mute the thread https://github.com/notifications/unsubscribe-auth/AAFW1Hw__gQ329y231XvKBozMfV9dn5Pks5uBN-sgaJpZM4U7Cod .

-- Regards, Ben Ford [email protected] +447540722690

commandodev avatar Jun 28 '18 14:06 commandodev

@fizruk

So yeah, true sum types are not supported well by Swagger 2.0 (and OpenAPI 3.0 AFAIK).

Hm, I thought OpenAPI 3.0 supported them (with oneOf) -- is there something there that makes you say that they're not supported "well"?

neongreen avatar Sep 22 '18 16:09 neongreen

@neongreen yes, you are right, oneOf is what we need for sum types and it is in OpenAPI 3.0: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/

I will edit my previous comment.

fizruk avatar Sep 24 '18 10:09 fizruk

Is there a plan for supporting OpenAPI 3.0 in this module? I think that the oneOf feature alone provides a lot of value.

AnthonySuper avatar Mar 28 '19 17:03 AnthonySuper

@AnthonySuper see #99

saschat avatar May 25 '19 08:05 saschat

Now that #99 has been closed, can we also close this one?

nsotelo avatar Sep 14 '20 09:09 nsotelo

Hi, Servant-swagger will be moved into the main Servant repo (see : https://github.com/haskell-servant/servant/pull/1475) If this issue is still relevant, would it be possible for you to summit it there? : https://github.com/haskell-servant/servant/issues

Thanks in advance!

akhesaCaro avatar Nov 17 '21 11:11 akhesaCaro