aeson icon indicating copy to clipboard operation
aeson copied to clipboard

Add some way (or document if it already exists) to extract data type name in 'defaultOptions'

Open chshersh opened this issue 7 years ago • 6 comments

I want to add full data type name to every field of my data type. Like this:

data ProjectManager = ProjectManager
    { projectManagerName   :: Text
    , projectManagerSalary :: Natural
    }

And I want to have JSON encoding like this: { name: "Vasya Pupkin", salary: 42 }. I don't see any way to implement general Options data type which can automatically work for any type with fieldLabelModifier because I don't know type name from context.

chshersh avatar Feb 23 '18 13:02 chshersh

That would be a good feature to have. I think adding the type name as an argument to fieldLabelModifier would be ideal (to allow users to adapt it to their own variations of prefix/suffix pattern), but for backwards compatibility that would probably be a new field instead.

In the meantime, a workaround for you may be to define wrappers around genericToJSON/genericParseJSON that use the type information from Generic to modify the fieldLabelModifier field.

Lysxia avatar Feb 23 '18 13:02 Lysxia

toJSON = genericToJSON defaultOptions { fieldLabelModifier = drop (length "ProjectManager") }

I've found something like this good enough for my use cases, you could go a step further and extract the type name with generics. Maybe you also want some checks to make sure that every field does indeed have that prefix?

bergmark avatar Feb 23 '18 14:02 bergmark

@bergmark

toJSON = genericToJSON defaultOptions { fieldLabelModifier = drop (length "ProjectManager") }

Well, this is not reliable enough. If I change type name I need not to forget all instances. Sure, this can be caught by roundtrip tests. But still extra few steps I would like to avoid if it's possible.

Your example can be type safe (the only cost is boilerplate) if some type family for extracting Symbol from type name existed, like this:

toJSON = genericToJSON defaultOptions 
    { fieldLabelModifier = drop $ length $ symbolVal $ Proxy @(TypeName ProjectManager) 
    }

This can be implemented shorter with simple utility function:

toJSON = genericToJSON defaultOptions { fieldLabelModifier = dropType @ProjectManager }

But looks like such type family doesn't exist:

  • https://stackoverflow.com/questions/48709079/how-to-convert-any-type-to-symbol-using-type-families

chshersh avatar Feb 23 '18 14:02 chshersh

Another workaround: If you are prefixing the fields because of name clashes you can use DuplicateRecordFields.

I'm all for adding something like dropType by the way, but with generics I think it should be possible to write it in such a way where you don't have to write the type name, and where you can't write another type name by accident. Maybe you should also be able to choose whether it's the type name or the constructor name you want to strip?

bergmark avatar Feb 23 '18 14:02 bergmark

Here's a function typeName that gives a String of the first data constructor in a generic type. It can also be extracted as a Symbol at the typelevel first. (I just wrote an answer to your SO question with all that.)

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeApplications #-}

import GHC.Generics

typeName :: forall a d f p. (Generic a, Rep a ~ D1 d f, Datatype d) => String
typeName = datatypeName (from @a undefined)

main = print (typeName @(Maybe Int))  -- "Maybe"

Lysxia avatar Feb 23 '18 15:02 Lysxia

After this GHC proposal is accepted and implemented, it becomes possible to write typeName function without Generic:

  • https://github.com/ghc-proposals/ghc-proposals/pull/164

chshersh avatar Sep 07 '18 04:09 chshersh