Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON keys examples in a Schema of (Map UUID Something)? #136

Open
michalrus opened this issue Dec 13, 2017 · 4 comments
Open

JSON keys examples in a Schema of (Map UUID Something)? #136

michalrus opened this issue Dec 13, 2017 · 4 comments

Comments

@michalrus
Copy link
Contributor

michalrus commented Dec 13, 2017

Sometimes it’s useful to return a Map k v from an API, e.g. when this k is some kind of a newtype over UUID, or similar.

Anyway, in such a case, the generated example value looks like:

    "accounts": {
      "additionalProp1": {
        "username": "string"
      },
      "additionalProp2": {
        "username": "string"
      },
      "additionalProp3": {
        "username": "string"
      }
    }

while the real response:

    "accounts": {
      "bff3d422-13be-48ad-b344-7e30d38c2c3f": {
        "username": "michalrus1"
      },
      "cbad52fd-96ae-4154-ad26-c8e8c8902a26": {
        "username": "michalrus2"
      }
    }

Perhaps it’d possible to have:

    "accounts": {
      "00000000-0000-0000-0000-000000000000": {
        "username": "string"
      },
      "00000000-0000-0000-0000-000000000000": {
        "username": "string"
      },
      "00000000-0000-0000-0000-000000000000": {
        "username": "string"
      }
    }

?

Now, the generated swagger.json contains:

        "accounts": {
          "additionalProperties": {
            "$ref": "#/definitions/Account"
          },
          "type": "object"
        }
@fizruk
Copy link
Member

fizruk commented Feb 21, 2018

Yes, this can be done using example property of schema. Something like this should work:

instance (ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (Map k v) where
  declareNamedSchema _ = case toJSONKey :: ToJSONKeyFunction k of
      ToJSONKeyText  _ _ -> declareObjectMapSchema
      ToJSONKeyValue _ _ -> declareNamedSchema (Proxy :: Proxy [(k, v)])
    where
      valueExample = toSchema (Proxy :: Proxy v) ^? example
      keyExample = case toSchema (Proxy :: Proxy k) ^? example of
        Just (String s) -> Just s
        _ -> Just "<key>" -- default key example can be useful when valueExample is not Nothing

      declareObjectMapSchema = do
        mKeyExample <- toSchema (Proxy :: Proxy k) ^? example
        schema <- declareSchemaRef (Proxy :: Proxy v)
        return $ unnamed $ mempty
          & type_ .~ SwaggerObject
          & additionalProperties ?~ schema
          & example .~ (toJSON . Map.singleton) <$> keyExample <*> valueExample

@michalrus would you like to submit a PR with examples constructed this way for various maps?

@michalrus
Copy link
Contributor Author

michalrus commented Sep 25, 2018

After another bug in our client code, I’m getting back to this issue. Sorry it took so long.

The example way is very nice! Thank you.

I’m thinking, could we also state somehow what type the keys are? For example if we have this:

newtype UserId = UserId UUID deriving (Eq, Generic)
newtype GroupId = GroupId UUID deriving (Eq, Generic)

instance ToSchema UserId
instance ToSchema GroupId

… and then if we make some endpoint return a Map UserId Text, and another one a Map GroupId Text, how to make this UserId visibile in the first one, i.e. how to distinguish those two responses?

We have that if newtypes are on the value side of maps. But keys?

@fizruk
Copy link
Member

fizruk commented Sep 26, 2018

We have that if newtypes are on the value side of maps. But keys?

In both Swagger 2.0 and OpenAPI 3.0 keys are strings.

The only way you can mention key type is via documentation (at least for most key types).

However, if your key type is an enumeration type and has finitely many values, you can do something like this:

-- | Produce a schema for a map with finitely many possible keys.
declareMapSchemaWithEnumBoundedKey
  :: forall k v. (Enum k, Bounded k, ToJSONKey k, ToSchema v)
  => Proxy k
  -> Proxy v
  -> Declare (Definitions Schema) Schema
declareMapSchemaWithEnumBoundedKey pk pv = do
  valueSchema <- declareSchemaRef pv
  case toJSONKey @k of
    ToJSONKeyValue _ _ -> error "keys should be strings!"
    ToJSONKeyText keyToText _ -> do
      let ps = map (\k -> (keyToText k, valueSchema))
                   [minBound..maxBound :: k]
      return $ mempty
        & type_ .~ SwaggerObject
        & properties .~ InsOrdHashMap.fromList ps

Here's usage example:

data Key = Up | Down | Space | Esc
  deriving (Generic, Show, Enum, Bounded, ToJSON)

instance ToJSONKey Key where
  toJSONKey = ToJSONKeyText (Text.pack . show) (Aeson.text . Text.pack . show)

type Action = String

newtype Controls = Controls (Map Key Action)

instance ToSchema Controls where
  declareNamedSchema _ = NamedSchema (Just "Controls")
    <$> declareMapSchemaWithEnumBoundedKey @Key @Action Proxy Proxy
>>> BSL8.putStrLn $ encodePretty $ toSchema @Controls Proxy
{
    "type": "object",
    "properties": {
        "Space": {
            "type": "string"
        },
        "Down": {
            "type": "string"
        },
        "Up": {
            "type": "string"
        },
        "Esc": {
            "type": "string"
        }
    }
}

@michalrus
Copy link
Contributor Author

michalrus commented Sep 26, 2018

Yes, clear.

So your first suggestion won’t work for complex types in values (which don’t have an example, i.e. when an example is implicitly constructed by Swagger-UI).

But I used the second suggestion to come up with this:

diff --git a/src/Data/Swagger/Internal/Schema.hs b/src/Data/Swagger/Internal/Schema.hs
index 927e9e6..b63895a 100644
--- a/src/Data/Swagger/Internal/Schema.hs
+++ b/src/Data/Swagger/Internal/Schema.hs
@@ -61,6 +61,7 @@ import Data.Version (Version)
 import Numeric.Natural.Compat (Natural)
 import Data.Word
 import GHC.Generics
+import Data.Typeable (Typeable, typeRep)
 import qualified Data.UUID.Types as UUID
 
 import Data.Swagger.Declare
@@ -520,18 +521,26 @@ instance ToSchema a => ToSchema (IntMap a) where
   declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy [(Int, a)])
 
 #if MIN_VERSION_aeson(1,0,0)
-instance (ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (Map k v) where
+instance (Typeable k, Typeable v, ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (Map k v) where
   declareNamedSchema _ = case toJSONKey :: ToJSONKeyFunction k of
       ToJSONKeyText  _ _ -> declareObjectMapSchema
       ToJSONKeyValue _ _ -> declareNamedSchema (Proxy :: Proxy [(k, v)])
     where
+      keyExample = case toSchema (Proxy :: Proxy k) ^. example of
+        Just (String s) -> s
+        _ -> "*"
+      keyType = T.pack . show . typeRep $ (Proxy :: Proxy k)
+      mapName = T.pack . show . typeRep $ (Proxy :: Proxy (Map k v))
       declareObjectMapSchema = do
-        schema <- declareSchemaRef (Proxy :: Proxy v)
+        valueSchema <- declareSchemaRef (Proxy :: Proxy v)
+        keySchema <- declareSchemaRef (Proxy :: Proxy k)
         return $ unnamed $ mempty
           & type_ .~ SwaggerObject
-          & additionalProperties ?~ schema
+          & description ?~ mapName
+          & properties .~ [(keyExample <> " @" <> keyType, valueSchema)]
+          & additionalProperties ?~ valueSchema
 
-instance (ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (HashMap k v) where
+instance (Typeable k, Typeable v, ToJSONKey k, ToSchema k, ToSchema v) => ToSchema (HashMap k v) where
   declareNamedSchema _ = declareNamedSchema (Proxy :: Proxy (Map k v))
 
 #else

So a description of that Map is set to its type name. Also we add a single artificial property which contains a sample key value (or *) and the key type name.

It’s not ideal, but enough for consumers to understand what’s happening. =)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants