Coder Social home page Coder Social logo

purescript-argonaut-codecs's Introduction

Argonaut Codecs

CI Release Pursuit

Argonaut is a collection of libraries for working with JSON in PureScript. argonaut-codecs provides codecs based on the EncodeJson and DecodeJson type classes, along with instances for common data types and combinators for encoding and decoding Json values.

You may also be interested in these other libraries from the Argonaut ecosystem:

The quick start will get you up and running with the basics of argonaut-codecs. For a deeper dive, please see the full documentation for this library, which includes an in-depth tutorial.

Installation

Install argonaut-codecs with Spago:

spago install argonaut-codecs

or install it as part of the Argonaut bundle:

spago install argonaut

Quick start

Use encodeJson to encode PureScript data types as Json and decodeJson to decode Json into PureScript types, with helpful error messages if decoding fails.

type User = { name :: String, age :: Maybe Int }

-- We get encoding and decoding for free because of the `EncodeJson` instances
-- for records, strings, integers, and `Maybe`, along with many other common
-- PureScript types.

userToJson :: User -> Json
userToJson = encodeJson

userFromJson :: Json -> Either JsonDecodeError User
userFromJson = decodeJson

In a REPL we can see these functions in action:

> type User = { name :: String, age :: Maybe Int }
> user = { name: "Tom", age: Just 25 }
> stringify (encodeJson user)
"{\"name\":\"Tom\",\"age\":25}"

> (decodeJson =<< parseJson """{ "name": "Tom", "age": 25 }""") :: Either JsonDecodeError User
Right { name: "Tom", age: Just 25 }

> res = (decodeJson =<< parseJson """{ "name": "Tom" }""") :: Either JsonDecodeError User
> res
Left (AtKey "age" MissingValue)

# You can print errors
> lmap printJsonDecodeError res
Left "An error occurred while decoding a JSON value:\n  At object key 'age':\n  No value was found."

Documentation

argonaut-codecs documentation is stored in a few places:

  1. Module documentation is published on Pursuit.
  2. Written documentation is kept in the docs directory.
  3. Usage examples can be found in the test suite.

If you get stuck, there are several ways to get help:

Contributing

You can contribute to argonaut-codecs in several ways:

  1. If you encounter a problem or have a question, please open an issue. We'll do our best to work with you to resolve or answer it.

  2. If you would like to contribute code, tests, or documentation, please read the contributor guide. It's a short, helpful introduction to contributing to this library, including development instructions.

  3. If you have written a library, tutorial, guide, or other resource based on this package, please share it on the PureScript Discourse! Writing libraries and learning resources are a great way to help this library succeed.

purescript-argonaut-codecs's People

Contributors

brandonhowe avatar carstenkoenig avatar cdepillabout avatar cryogenian avatar davezuch avatar dgendill avatar elliotdavies avatar foresttoney avatar garyb avatar hdgarrood avatar i-am-the-slime avatar jamieballingall avatar jdegoes avatar jordanmartinez avatar jvliwanag avatar jwhiles avatar kl0tl avatar kritzcreek avatar leighman avatar lucianu avatar milesfrain avatar natefaubion avatar ntwilson avatar passy avatar sigma-andex avatar srghma avatar th-awake avatar thomashoneyman avatar vaibhavsagar avatar zudov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

purescript-argonaut-codecs's Issues

suggestion to replace `"value"` with `"contents"` in `Either` decoder, encoder

Is your change request related to a problem? Please describe.

In Haskell's Aeson library, the default contentsFieldName is "contents":
https://hackage.haskell.org/package/aeson-2.2.1.0/docs/Data-Aeson.html#v:defaultTaggedObject

In argonaut-codecs, it appears the default is "value":

val <- note (AtKey "value" MissingValue) $ FO.lookup "value" obj

Examples:
https://github.com/coot/purescript-argonaut-aeson-generic depends on argonaut-codecs and attempts to provide compatibility between PureScript's Argonaut and Haskell's Aeson. It uses "contents". For most data types, it works great. However, it cannot decode/encode Either to be compatible with Haskell's Aeson.
I have provided an example in this issue: coot/purescript-argonaut-aeson-generic#22

I have attempted to fix this on the Haskell side by overriding the instances to use "value", but haven't managed to make this work: https://github.com/peterbecich/purescript-bridge/blob/55e265b0d44c001357cd3aef3ac4a894128df12f/example/src/Types.hs#L96

Describe the solution you'd like
My request is to change "value" to "contents" here:

val <- note (AtKey "value" MissingValue) $ FO.lookup "value" obj

and in the encoder

I have tested this: eskimor/purescript-bridge@55e265b This change fixes argonaut-aeson-generic. Either is encoded and decoded successfully.

Additional context
I am attempting to update Purescript Bridge and encountered this issue: eskimor/purescript-bridge#89

Thank you

Decode error reporting

The majority of decoders in 'Data.Argonaut.Decode' complain about errors without providing context for the error. For example Couldn't decode StrMap: Value is not a String. When parsing a large JSON blob with a lot of different structures in it, it's quite hard to figure out what exactly is the problem. Some decoders though, do provide at least the value which triggered the error:

maybe (Left $ "Expected character but found: " <> show j) Right

I can submit a PR doing the same for the rest of the decoders.

Decoding into Map key value expects an array instead of an object

Is your change request related to a problem? Please describe.
It's more of a question than a change request per se. I was naively trying to decode a JSON object by giving it the type Map String T (for some irrelevant T). This didn't work, because the decodeJson instance for Map expects an Array (of pairs, I guesss).

Isn't it a bit more obvious to expect a JSON object instead of an array of pairs?

During generic encoding/decoding existing instances of EncodeJson are ignored

e.g. Nothing is not encoded as Null, the EncodeJson instance for Maybe is completely ignored when generically traversing a data structure containing a Maybe. This is because gEncodeJson' recursively invokes itself without using EncodeJson at all.

The result is, that generic encoding is very limited, because it is not possible to manually override parts of the process.

As gEncodeJson' only operates on spines, I don't really see how this can be fixed, unfortunately. Except for calling fromSpine on each recursion and checking whether there is an instance somehow and using it, if available. While this could theoretically work with some JS hackery, I believe fixing decoding is much harder, because you can not retrieve a value before having already decoded the JSON, but then it is already too late. (Except we could decode it twice if an instance is found.)

In any case - this is very hacky. Is this afterall because Haskell's generics are more powerful, that aeson does not have this problem?

Instances for NonEmpty Array and NonEmpty List

Would it make sense to have instances for NonEmpty Array and NonEmpty List?

instance encodeJsonNonEmptyArray :: (EncodeJson a) => EncodeJson (NonEmpty Array a) where
  encodeJson (NonEmpty h t) = encodeJson $ cons h t

instance encodeJsonNonEmptyList :: (EncodeJson a) => EncodeJson (NonEmpty List a) where
  encodeJson (NonEmpty h t) = encodeJson $ cons h (toUnfoldable t)

`Map String a` <-> Object

Following the discussion here #101

Could an EncodeJson / DecodeJson for Map String a be provided that convert from/to a javascript object instead of an array?

I think that this is the desired behavior in nearly all situations and when it isn't people can manually write their codec.

Provide a way to override generic encoding/decoding for certain data types

For encoding Nothing as null for example, or Tuple a b as [a, b], ....
We could include options in Data.Argonaut.Options.Options, called userEncoding, userDecoding:

userEncoding :: GenericSignature -> GenericSpine -> Maybe Json
userDecoding :: GenericSignature -> Json -> Maybe GenericSpine

or something like this. genericEncodeJson'/genericDecodeJson' would then try those functions first on each recursion. Thus, the user can choose to override the encoding/decoding for certain signatures.

Instance for NonEmptyArray?

We have an instances of EncodeJson and DecodeJson for NonEmpty Array but not for NonEmptyArray. Is there a reason for that?

If not, I'd be happy to take a shot at implementing, with two minor points:

  1. The logical names for those instances (encodeJsonNonEmptyArray and decodeJsonNonEmptyArray) are already in use. I propose renaming the existing ones to encodeJsonNonEmpty_Array and decodeJsonNonEmpty_Array. Would that break anything?
  2. There was some discussion in #25 about implementing encodeJsonNonEmptyFoldable and decodeJsonNonEmptyFoldable. That doesn't seem to have happened. Was there a reason it was avoided or did it just fall off the radar?

Deprecate .:? (getFieldOptional) and .!= (defaultField)

After speaking with @davezuch I realized (at his prompting) that the functions and operators for .:? and .!= are unnecessary and can be replaced without losing any functionality in this library.

For example, given this type:

newtype User = User
  { name :: String 
  , age :: Maybe Int
  , team :: String
  }

derive instance newtypeUser :: Newtype User _
derive newtype instance showUser :: Show User 

these two instances are identical:

instance decodeJsonUser :: DecodeJson User where
  decodeJson json = do
    obj <- decodeJson json
    name <- obj .: "name"
    age <- obj .:? "age"
    team <- obj .:? "team" .!= "Red Team"
    pure $ User { name, age, team }
    
instance decodeJsonUser :: DecodeJson User where
  decodeJson json = do
    obj <- decodeJson json
    name <- obj .: "name"
    age <- obj .: "age"
    team <- obj .: "team" <|> pure "Red Team"
    pure $ User { name, age, team }

Because the Maybe instance for .: already covers the case covered by .:?, and the functionality of .!= (providing a default value for a type which may not exist in the Json, but must exist in the decoded type) is covered by the use of Alternative: <|> pure default. In fact, this is even better, because it introduces to folks the ability to have fallbacks via Alternative rather than lead them to use an overly-specific operator.

> decodeJson =<< jsonParser """{ "name": "Tom", "age": 55, "team": "Blue Team" }""" :: Either String User
(Right { age: (Just 55), name: "Tom", team: "Blue Team" })

> decodeJson =<< jsonParser """{ "name": "Tom", "age": 55 }""" :: Either String User
(Right { age: (Just 55), name: "Tom", team: "Red Team" })

> decodeJson =<< jsonParser """{ "name": "Tom", "age": null }""" :: Either String User
(Right { age: Nothing, name: "Tom", team: "Red Team" })

Side note: I'm not sure that .:! needs to exist either. Its sole difference from .: applied to Maybe is that it will error on a key present with a null value instead of decoding to Nothing. I'm not sure when that would ever be the desired behavior. But that's a question for another day.

I'd like to open a PR which deprecates .:? and .!= and standardizes on just .: and <|> pure default.

Should Maybe codec return `Nothing` for nulls?

If you're writing a codec for a value that cannot have a DecodeJson instance declared for it, it seems sensible to write something like this:

traverse decodeCustom =<< obj .? "prop"

To get a Maybe Custom success result. The idea being obj .? "prop" result is a Maybe Json and then decodeCustom does the Json -> Custom step. However, the decodeJsonJson instance always succeeds, as null is a decodeable value, so you end up with Just null as the response, and then decodeCustom fails as it's probably expecting something else.

If you're using instances everywhere this works fine, as the custom decoder would run "inside" the Maybe decoder, and failing there gives you a Nothing result.

Maybe it was a faulty assumption on my part that null values would be considered Nothing? It took me a really long time to track this down though. 😭

easily encode/decode records

Is there a way to easily encode and decode records?

For instance, I have the following type:

newtype Register = Register { email :: String
                            , password :: String
                            }

I'd like to use purescript-generics for the encoding and decoding like below:

derive instance genericRegister :: Generic Register
instance encodeJsonRegister :: EncodeJson Register where
    encodeJson = gEncodeJson

But when running the following code:

let register = Register { email: "[email protected]", password: "foobar" }
in show $ encodeJson res

I get the following output: {"values":[{"password":"foobar","email":"[email protected]"}],"tag":"Register"}
I was expecting the output to look like this: {"password":"foobar","email":"[email protected]"}

I tried to change the EncodeJson instance like the following:

instance encodeJsonRegister :: EncodeJson Register where
    encodeJson (Register reg) = gEncodeJson reg

But that just gave me the following compiler error:

Error found:
Error in value declaration encodeJsonRegister:
Error at /opt/src/src/Main.purs line 44, column 1 - line 46, column 1:
Error in module Main:
No instance found for

  Data.Generic.Generic { password :: String
                       , email :: String
                       }

The error makes sense, but I was hoping it would be easier to define encodeJson for records. Is there an easy way? I don't want to have to write out something like this for every type:

  instance encodeJsonRegister :: EncodeJson Register where
    encodeJson (Register reg)
      =  "email" := reg.email
      ~> "password" := reg.password
      ~> jsonEmptyObject

Maybe I've just been spoiled by aeson :-(

Recursive newtype in not deriving

Here is a simple modified example from docs with recursive newtype, and it is not deriving:

newtype AppUser = AppUser { name :: String, age :: Maybe Int, appUser :: AppUser }

derive instance newtypeAppUser :: Newtype AppUser _

derive newtype instance decodeJsonAppUser :: DecodeJson AppUser -- error
The value of decodeJsonAppUser is undefined here, so this reference is not allowed.
PureScript(CycleInDeclaration)

If it works as intended, probably it worth mentioning in the docs.

Address breaking change in typelevel-prelude dependency for PureScript v0.13.0

This package needs an update because the typelevel-prelude dependency has had a breaking release (to address a re-export issue in the compiler, purescript/#3502). The relevant change is in typelevel-prelude/#48. Ideally this update can be done without requiring a breaking release of this library by ensuring the package works with both 4.x.x and 5.x.x of typelevel-prelude.

Issue purescript/#3650 tracks the various libraries affected by the change.

An instance for nested tuples?

I recently figured out how to get nested tuples to encode and decode as JSON arrays
e.g.

1 /\ "hi" /\ [ true ] /\ { a: 0 } -> [ 1, "hi", [ true ], { "a": 0 } ]

I did this for simple-json but I'm pretty sure the approach would work here as well. Unexpectedly to me, the PR was not accepted. In light of that, I figured I'd ask if this would be a welcome addition before I put in the work.

Add `fromJson` and `toJson`

The first thing I always need to do when using argonaut in a project is to write functions fromJson function which composes parseJson and decodeJson and toJson composing encodeJson and stringify:

fromJson :: forall t. DecodeJson t => String -> Either JsonDecodeError t
fromJson = parseJson >=> decodeJson

toJson :: forall t. EncodeJson t => t -> String
toJson = encodeJson >>> stringify

So why not add them to argonaut? They could also be named parseAndDecode/ encodeAndStringify or sth else, I don't really mind. Though I find fromJson and toJson just nicely minimalistic.

I have created a pr for it in #109 .

Update:
changed type variable from json to t in the first case to avoid confusion.

getFieldOptional possibly broken in nested structures?

Here's the minimal example to reproduce:

newtype Foo = Foo
  {fooId :: Int
  ,bazList :: List Baz
  }

instance decodeJsonFoo :: DecodeJson Foo where
  decodeJson json = do
    obj <- decodeJson json
    id <- obj .? "fooId"
    bs <- obj .? "bazList"
    pure $ Foo { fooId: id
               , bazList : bs}


newtype Baz = Baz
  {bazId :: Int
  ,bazOpt :: Maybe Int
  }

instance decodeJsonBaz :: DecodeJson Baz where
  decodeJson json = do
    obj <- decodeJson json
    bId   <- obj .? "bazId"
    bad   <- obj .?? "bazOpt"
    good <- foldJsonNumber Nothing (Just <<< floor) <$> obj .? "bazOpt"
    pure $ Baz { bazId: bId
               , bazOpt: bad
               }

When each Baz in the list has Int value in bazOpt then "bad" works fine.
When some Bazes in the list has null in bazOpt field then "bad" give this error:
Couldn't decode List Value is not a Number.

"good" works fine even with nulls.

is getFieldOptional broken?

Split instances out for Generic into a standalone library

Since the situation with generics is still up in the air. We can create a generics-rep version too. It'll need newtypes or something, to prevent the instances being orphaned, however (or use of the gEncodeJson / gDecodeJson directly).

Unfancy JSON transcoder

I'm struggling to transcode a record with native types and a single foreign Json entry for round trip to localStorage. My naive untested yet solution is:

-- I want to transcode this
newtype Event = Event { id: String, data: Json }

newtype UnsafeJson a = Unsafe Json a

instance encodeJsonUnsafe Json :: EncodeJson (Unsafe Json a) where
  encodeJson = unsafeCoerce

instance decodeJsonUnsafe Json :: DecodeJson (Unsafe Json a) where
  decodeJson = Left <<< unsafeCoerce
  1. What would be preferable?
  2. Could instances be provided for some newtype that transcode without necessarily doing anything smart? I know I can manually stringify/parse but it wouldn't work with clients of the codecs (specifically https://github.com/texastoland/purescript-localstorage/blob/refactor/src/DOM/WebStorage/JSON.purs).

Add example to readme of automatic decoding into simple record types

I heard this library now has automatic decoding into a plain record type, like simple-json is known for.
#46

It would be nice to see in the README that this library has this functionality, and to see an example of how to use it.

I guess it would be used something like this:

testDecode :: Maybe { a :: Int, b :: String }
testDecode = decodeJson """ { a: 123, b: "xyz" } """

Wrong type shown in docs

Describe the bug
In Pursuit, the type of decodeJson (as listed on the DecodeJson typecclass) is shown as Json -> Either String a. However, it is Json -> Either JsonDecodeError a in the code.

Expected behavior
An updated documentation, this is the only error that I have come across thus far, however, there could be other errors.

Feature Request: Enable the default record serialization to be customized

Since an EncodeJson instance was added for Records (#37), it is not possible to customize the default serialization of records without resorting to newtypes for everything. This is somewhat inconvenient for simple transformations.

It would be cool if I could pass a function to apply custom processing logic to the output JSON. In my case, I would like to be able to capitalize the names of the keys. Post-processing for keys would be a good first step, since I imagine it's fairly common to need to transform them into other formats (e.g. underscore separated).

Add instance for Ratio?

I want to encode and decode values of type Ratio. It is not supported yet. My offer is to encode and decode the same way as Aeson:

λ encode (3 % 5 ∷ Ratio Int)
"{\"numerator\":3,\"denominator\":5}"

My case specifically is related to argonaut-aeson-generic and purescript-bridge, so it is important to me that the encoding matches the Haskell side of my code. Since PureScript does not support orphan instances, I have no other way of making stuff work.

I can write a patch in no time.

add decoder for NonEmptyString

can add after 0.14, because cannot run tests now

instance decodeJsonCodePoint :: DecodeJson NonEmptyString where
  decodeJson = decodeNonEmptyString

decodeNonEmptyString :: Json -> Either JsonDecodeError NonEmptyString
decodeNonEmptyString json =
  note (Named "NonEmptyString" $ UnexpectedValue json)
    =<< map (NonEmptyString.fromString) (decodeString json)

Support decoding of undefined record field to Nothing

Is your change request related to a problem? Please describe.

Can we support optional fields to Maybe? In particular, treat undefined values as Nothing.

Both should be decode to { a: Nothing }

  • {}
  • {"a": null}

Describe the solution you'd like

I propose adding a new

class DecodeJsonField a where 
  decodeJsonField :: Maybe Json -> Maybe (Either JsonDecodeError a)

and using it within the decoding.

I am also of the opinion of encoding Nothing should encode to an undefined field - but that can be a separate matter.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.