How to decode JSON into extensible sum type or variant

PureScript beginner here.

Let’s assume the following API response

testJSON1 :: String
testJSON1 = """
    -- | A
    { "apple": "Hello"
    , "banana": [ 1, 2, 3 ]
    -- | B
    { "bax": 5 },
    -- | C
    { "foo": "bar"
    , "bax": "now it's a string" 

which I could model with a sum type. But let’s say that my code only knows how to handle A and B and doesn’t care about C and all other possible shapes.

How would I best represent this in PureScript and also decode it, ideally with Simple.JSON?

A sum type wouldn’t work because every time a new possible thing is added (D, E,…) I’d have to update my sum type.

I also can’t just make it an extensible record, because here the key bax has a different type depending on which thing it is.

At first variants sounded like something that would work but according to purescript-option they’re apparently about key/value rather than optional records as a whole:

Variants capture the idea of a collection of key/value pairs where exactly one of the key/value pairs exist. E.g. Data.Variant.Variant (foo :: Boolean, bar :: Int) means that either only foo exists with a value or only bar exists with a value, but not both at the same time.

Options capture the idea of a collection of key/value pairs where any key and value may or may not exist. E.g. Option.Option (foo :: Boolean, bar :: Int) means that either only foo exists with a value, only bar exists with a value, both foo and bar exist with values, or neither foo nor bar exist.

so I guess I need something like type Thing = forall v. A | B | v. I really thought that a variant

type A
  = { foo :: String }

type Things
  = forall v. Variant ( a :: A | v )

would work but I don’t know how to make this work with Simple.JSON. It’s current variant handling seems to expect objects of type { type: ..., value: ...}, for the variants unless I’m misunderstanding something here.

Maybe I’m overlooking some excellent documentation here but I couldn’t find any guidance on how to only handle a subset of objects in an array from a JSON response with PureScript.

EDIT: I guess I could just define a newtype wrapper and just implement my own decoding logic where I try to decode each object in the array into a known sumtype and if it fails I just ignore it. But I’d have to repeat this for every such array of objects and I’m sure there’s a nicer generic solution.

Is that necessarily a problem? If you want your code to be able to handle a new type of thing in this array sensibly, you’ll likely need to make changes to your code anyway, so I would recommend taking the opportunity to define the shape of what your code is expecting in a way that allows the compiler to check things for you.

I don’t know that there is necessarily a nicer generic solution outside of the things you’ve already listed. It might be easier to help if you gave a bit more context.

I don’t want the new types to be handled though, I just want to ignore them. Imagine a marketing department that adds new content types in a CMS, but the static site generator consuming these types can just keep working until a developer comes along and implements these new content types. In the meantime, other programs in the same organization can already work with these new content types. It’s a bit of a contrived example but I can’t currently think of more context than that.

I have quite a lot of cases where I decode something and only care about a subset of the things, just like I often only care about a subset of keys, which PureScript handles very nicely already.

Ah, ok, in that case how about decoding first to Array Json and then feeding that into this function:

-- | Try to decode each item in a Json array, and ignore those which don't decode.
tryDecodeArray :: DecodeJson a => Array Json -> Array a
tryDecodeArray = Array.mapMaybe (hush <<< decodeJson)

That looks pretty cool, I didn’t know I could just decode into Json. But… won’t this make it hard to discern intended failures (unknown types we ignore) and unintended failures (a type we want to handle but which failed to decode)?

Yes, it will just ignore everything which fails to parse. You can probably adapt that function to distinguish intended versus unintended failures, but we’ll need more context to be able to help you with that.

I can actually just try to decode one or several fields manually and use them to decide into which of my types the entire object should be decoded.

Edit: or I stop wildly overcomplicating things and I just create a type that will handle all variations and then discard the variations I don’t need when transforming things to my domain model