biocad / openapi3

OpenAPI 3.0 data model
BSD 3-Clause "New" or "Revised" License
39 stars 54 forks source link

Generic polymorphic schema handle lists differently to aeson #58

Open brprice opened 2 years ago

brprice commented 2 years ago

Consider a polymorphic type data T a = T [a], where the parameter is used in a list. The generically-derived instance ToSchema (T Char) agrees with ToJSON (encodes as a string), but the polymorphic version instance ToSchema (T a) does not (when used at a ~ Char -- it encodes as an array of "characters" i.e. length-1 strings.).

A complete example

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}

module Main where

import Data.Aeson
import Data.OpenApi
import GHC.Generics

data Poly a = Poly [a]
 deriving (Generic,Show)

data Mono a = Mono [a]
 deriving (Generic,Show)

instance ToJSON a => ToJSON (Poly a)
instance ToSchema a => ToSchema (Poly a)

instance ToJSON (Mono Char)
instance ToSchema (Mono Char)

main :: IO ()
main = do
  putStrLn "Testing monomorphic instance"
  go $ Mono "foo"
  putStrLn ""
  putStrLn "Testing polymorphic instance"
  go $ Poly "foo"
 where
  go x = do
    case validatePrettyToJSON x of
      Nothing -> putStrLn "validation passed"
      Just err -> putStrLn "validation FAILED" >> putStrLn err

which results in

Testing monomorphic instance
validation passed

Testing polymorphic instance
validation FAILED
Validation against the schema fails:
  * expected JSON value of type OpenApiArray

JSON value:
"foo"

Swagger Schema:
{
    "items": {
        "example": "?",
        "maxLength": 1,
        "minLength": 1,
        "type": "string"
    },
    "type": "array"
}

Swagger Description Context:
{}

This is presumably because openapi3 and aeson handle the special case of [Char] differently. (There may be some other such cases that differ -- I have not looked further. #50 is perhaps related.)

maksbotan commented 1 year ago

I fear that once instance ToSchema (Poly a) is generated, there is no way to "look" at concrete instantiations of a. Some horrible magic is required to get it right.

Is this case important for you, or just a peculiarity?

brprice commented 1 year ago

Is this case important for you, or just a peculiarity?

I ran across this problem in a real project. Thankfully I can work around this bug by listing out the particular monomorphic instances I need.

I fear that once instance ToSchema (Poly a) is generated, there is no way to "look" at concrete instantiations of a. Some horrible magic is required to get it right.

Sure, you cannot "look" in this way. We could (with only a slightly more complex class) generate the correct polymorphic instance in the first place. The way this tends to be done (e.g. aeson and base's Show) is to add an extra member to the class toJSONList :: [a] -> Value, showList :: [a] -> [ShowS] which is then used in the general instance C a => instance C [a]. (Note that this extra member can have a default definition, so there is no extra burden when defining members of the class, unless one wants to handle lists peculiarly).

(Perhaps this is what you meant by "horrible magic"?)