We don't do type classes for this purpose. If you have to interface with some JSON with predefined schema, we just write the instances manually. It's not that hard at all—usually it's just calling withObject with a series of lifted function applications `Constructor <$> (o .: "field1") <*> (o .: "field2")`. Remember the Parser type is a Functor/Applicative/Monad/Alternative/MonadPlus so there's a whole host of utilities for these classes that make writing such instances both simple and concise. Of course if you are doing simple things like removing the leading underscore on lenses data typed, just use TH to derive the instance, passing slightly modified `Options`.
If you need to manually handle tags, here's a snippet that can help you:
-- | Safely accesses a JSON object where the value at a key is text. It takes an
-- object, a key, and a continuation of what to do when this key is present.
--
-- Example:
--
-- @
-- data D = A | B | C Int
-- instance FromJSON D where
-- parseJSON = withObject "D" $ \o ->
-- o .:=> "tag" $ \case
-- "A" -> pure A
-- "B" -> pure B
-- "C" -> C <$> (o .: "theInt")
-- t -> fail $ "Unrecognized tag in type D: " ++ unpack t
-- @
(.:=>) :: Object -> Text -> (Text -> Parser a) -> Parser a
o .:=> k = \m -> o .: k >>= withText ("Object with mandatory key " <> unpack k) m