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

reuse toJSON instances #127

Open
Pitometsu opened this issue Oct 10, 2017 · 6 comments
Open

reuse toJSON instances #127

Pitometsu opened this issue Oct 10, 2017 · 6 comments

Comments

@Pitometsu
Copy link

Pitometsu commented Oct 10, 2017

E.g. I have type

data PaymentOptionsCommon id =
  PaymentOptions
    { poId          :: id
    , poPaymentType :: Maybe PaymentMethod
    , poPaymentNet  :: Maybe Int
    , poInstallment :: Maybe Int  -- days
    } deriving (Generic, Eq, Ord, Show)

type PaymentOptions   = PaymentOptionsCommon ()

And ToJSON instance like this:

instance FromJSON PaymentOptions where
  parseJSON = withObject "paymentOptions" $ \o -> do
    poId          <- skip
    poPaymentType <- o .:? "type"
    poPaymentNet  <- o .:? "deffered"
    poInstallment <- o .:? "installment"
    return $! PaymentOptions{..}

instance ToJSON PaymentOptions where
  toJSON PaymentOptions{..} =
    object
      [ "type"        .= poPaymentType
      , "deffered"    .= poPaymentNet
      , "installment" .= poInstallment
      ]

And auto-generated ToSchema instance:

instance ToSchema PaymentOptions

So, swagger would looks like:

  PaymentOptionsCommon:
    required:
      - poId
    properties:
      poId:
        items: []
        type: array
      poPaymentType:
        $ref: '#/definitions/PaymentMethod'
      poPaymentNet:
        maximum: 9223372036854776000
        minimum: -9223372036854776000
        type: integer
      poInstallment:
        maximum: 9223372036854776000
        minimum: -9223372036854776000
        type: integer
    type: object

But instead I expect to see something more like:

  PaymentOptions:
    properties:
      type:
        $ref: '#/definitions/PaymentMethod'
      deffered:
        maximum: 9223372036854776000
        minimum: -9223372036854776000
        type: integer
      installment:
        maximum: 9223372036854776000
        minimum: -9223372036854776000
        type: integer
    type: object

Without prefixes

The question is: how to do this?

@Pitometsu
Copy link
Author

@fizruk can you suggest solution, please?

@phadej
Copy link
Collaborator

phadej commented Oct 10, 2017

If you write ToJSON by hand, you have to write ToSwagger by hand as well.

@fizruk We should add that note, and an example to http://hackage.haskell.org/package/swagger2-2.1.5/docs/Data-Swagger.html#g:4, shouldn't we?

@Pitometsu
Copy link
Author

like

instance ToSchema PaymentOptions where
  declareNamedSchema _ = do
    mIntSchema         <- declareSchemaRef (Proxy :: Proxy (Maybe Int))
    mPaymentTypeSchema <- declareSchemaRef (Proxy :: Proxy (Maybe PaymentMethod))
    return $ NamedSchema (Just "PaymentOptions") $ mempty
      & type_ .~ SwaggerObject
      & properties .~
      [ ("type",        mPaymentTypeSchema)
      , ("deffered",    mIntSchema)
      , ("installment", mIntSchema)
      ]
      & required .~ []

@fizruk
Copy link
Member

fizruk commented Oct 11, 2017

@Pitometsu yes, that seems like a valid instance. Just two comments:

  • Schema for Maybe Int is the same as for Int. The difference can only be seen when using Generic-based deriving — then Maybe-fields won't show up as required. When using declareSchemaRef it really should be the same, so you can just drop Maybe.
  • required .~ [] is redundant.

FromJSON and ToJSON instances only handle decoding/encoding, however, that's not enough to construct a Schema, because parseJSON and toJSON methods are not inspectable. So yes, you need to write your own ToSchema instance and make sure it matches ToJSON (and FromJSON).

You have a few options to write your own Schema:

instance ToSchema PaymentOptions where
  declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions
    { fieldNameModifier = f }
    where
      f "poPaymentType" = "type"
      f "poPaymentNet"  = "deffered"
      f "poInstallment" = "installment"
      f name = name

I myself rely mostly on the second option (Generic-based deriving). Normally I would use some field naming convention + SchemaOptions and aeson's Options that match each other and derive all FromJSON, ToJSON and ToSwagger using those options.

In any case even with deriving mechanisms you need to ensure that ToSchema really matches ToJSON. For that I use validateToJSON. Usually inside a QuickCheck property.

If you also use servant, consider servant-swagger's Servant.Swagger.Test. Specifically validateEveryToJSON runs validateToJSON tests for every request/response JSON in your entire API! See test suite example here.

@Pitometsu
Copy link
Author

Thank you a lot for detailed answer, things become much more clear now.

@fizruk
Copy link
Member

fizruk commented Feb 22, 2018

I think #127 (comment) should be added somewhere in the How to use this library section.

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

3 participants