Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling `id` in derived Aeson FromJSON instances with Aeson/JSON

If I have JSON and I try to derive the FromJSON instances automatically with Generics, I run into problems with id existing in more than one place in the JSON.

Is there a way for me to override just the id part or do I have to write the whole instance in order to change those particular entries? The JSON actually has more fields, but I have left most of it out in this example. So it is actually rather tedious to write out the whole FromJSON instance.

JSON:

{
  "response": [
    {
      "id": 1,
      "brandId": 1,
      "productTypeId": 1,
      "identity": {
        "sku": "x",
        "barcode": "Ax"
      },
      "stock": {
        "stockTracked": false,
        "weight": {
          "magnitude": 0
        },
        "dimensions": {
          "length": 0,
          "height": 0,
          "width": 0,
          "volume": 0
        }
      },
      "financialDetails": {
        "taxable": false,
        "taxCode": {
          "id": 1,
          "code": "x"
        }
      },
... etc
  ]
}

CODE So far:

data Response = Response
    { response :: [Body]
    } deriving (Show,Generic)

data Body = Body
    { id                    :: Int
    , brandId               :: Int
    , productTypeId         :: Int
    , identity              :: Identity
    , productGroupId        :: Int
    , stock                 :: Stock
    , financialDetails      :: FinancialDetails
    } deriving (Show,Generic)                  

data Identity = Identity
    { sku       :: String
    , ean       :: String
    , barcode   :: String
    } deriving (Show,Generic)                  

data Stock = Stock
    { stockTracked  :: Bool
    , weight        :: Weight
    , dimensions    :: Dimensions
    } deriving (Show,Generic)                  

data Weight = Weight
    { magnitude  :: Int
    } deriving (Show,Generic)                  

data Dimensions = Dimensions
    { length :: Int
    , height :: Int
    , width  :: Int
    , volume :: Int
    } deriving (Show,Generic)                  

data FinancialDetails = FinancialDetails
     { taxable :: Bool
     , taxCode :: TaxCode
     } deriving (Show,Generic)                  

data TaxCode = TaxCode
     { id      :: Int
     , code    :: String 
     } deriving (Show,Generic)                  


instance FromJSON Response
instance FromJSON Body
instance FromJSON Identity
instance FromJSON Stock
instance FromJSON Weight
instance FromJSON Dimensions
instance FromJSON FinancialDetails

This gives the error:

[1 of 1] Compiling Main             ( reponse.hs, interpreted )

response.hs:73:8:
    Multiple declarations of `id'
    Declared at: response.hs:19:7
                 response.hs:73:8
Failed, modules loaded: none.

Ideally I would like to change the first id to body_id and the second to taxCode_id without having to write out the whole instance.

like image 685
matt Avatar asked Feb 29 '16 14:02

matt


3 Answers

When deriving FromJSON instances you can pass an option to the genericParseJSON function. It's usually

data Foo = {- ... -} deriving (Show, Generic)

instance FromJSON Foo where
    parseJSON = genericParseJSON defaultOptions
    -- defaultOptions :: Options

while you can replace defaultOptions with an Option you constructed manually. The Option type have a field fieldLabelModifier that can preprocess the field name of your data type. You can define your data type as

data Body = Body
  { body_id :: Int
  ...

And write a helper function which maps "body_id" to "id" and anything else unchanged:

body_noprefix "body_id" = "id"
body_noprefix s = s

Then define the instance as

instance FromJSON Body where
  parseJSON = genericParseJSON (defaultOptions { fieldLabelModifier = body_noprefix })
like image 103
zakyggaps Avatar answered Oct 21 '22 15:10

zakyggaps


The problem isn't that GHC can't provide Generics, but that in Haskell record labels are also accessor functions and thus you get a name clash if you try to use the same label for two different records.

If you use the the functions from Data.Aeson.TH you can use fieldLabelModifier option to eg. remove prefixes from your labels.

data Identity = Identity
{ identitysku       :: String
, identityean       :: String
, identitybarcode   :: String
} deriving (Show)

$(deriveJSON defaultOptions{fieldLabelModifier = drop (length "identity")} ''Identity)

This code requires the {-# LANGUAGE TemplateHaskell #-} pragma.

like image 20
Christoph Hegemann Avatar answered Oct 21 '22 13:10

Christoph Hegemann


Others have given great ways to modify the generated instances, which will sometimes be the best thing to do. It's not, however, your only choice. You also have the option of defining one or more of the types in separate modules, generating instances in those modules, and then importing the modules qualified or otherwise using qualified names to refer to the overlapping field names.

like image 2
dfeuer Avatar answered Oct 21 '22 13:10

dfeuer