I have a complex JSON which can have variable sub-JSONs inside it, each with their own schema. Here is an illustration of its structure:
[
"node_level": "unit",
"name": "unit 1",
"description": "the first unit",
"theme_image_path": "hello.png",
"children": [
{
"node_level": "chapter",
"name": "chapter 1",
"theme_image_path": "dog.png",
"children": [
{
"node_level": "lesson",
"lesson_type": "reading",
"children": [
{
"node_level": "activity",
"activity_type": "vocabulary",
"elements": { /* this bit varies systematically according to activity_type */ }
}
]
}
]
}
]
]
Note that the activity level could be one of a number of different object schemas - effectively each activity type has its own schema that should fit in here, with activity_type and elements varying.
Here is my current implementation, which is both not working with the purescript-argonaut-codecs library (error shown below), and not fulfilling some of my aims (detailed below) even if it would work.
import Data.Argonaut.Encode (encodeJson)
-- This is raising errors atm
contentToJson :: Content -> Json
contentToJson = encodeJson
type UnitNode =
{ node_level :: String -- would prefer this value to be enforced to "unit" (see point 3 below)
, name :: String
, description :: String
, theme_image_path :: String
, children :: Array ChapterNode
}
type ChapterNode =
{ node_level :: String -- prefer enforced to "chapter" (point 3)
, name :: String
, theme_image_path :: String
, children :: Array LessonNode
}
type LessonNode =
{ node_level :: String -- prefer enforced to "lesson" (point 3)
, lesson_type :: String -- would prefer an enum-like situation (see point 1 below)
, children :: Array ActivityNode
}
type ActivityNode =
{ node_level :: String -- prefer enforced to "activity" (point 3)
, layout :: String -- prefer enum-like (point 1)
, elements :: VocabFlashcard -- this should be one of many types (see point 2 below)
}
type VocabFlashcard =
{ audio :: String
, image :: String
, vocabText :: String
, translation :: String
}
There are a few things I am struggling with getting done in Purescript. They are mostly doable in Typescript, so if I can't do them in Purescript I will consider switching back...
1. Enum-like fields
Some fields (node_level, lesson_type, activity_type) take strings which are of a given closed subset, e.g. node_level can only be one of "unit", "chapter", "lesson", or "activity". With TS I would do something like this:
type NodeLevel = "unit" | "chapter" | "activity" | "lesson"
With Purescript I have tried two methods, they both have their problems:
- Use
newtype - Use an enum type like
data NodeLevel = UnitLevel String | ChapterLevel String | ActivityLevel String | LessonLevel Stringand then separately define the strings appropriate to each (this is pretty verbose)
The first method is ok without the codec, but the codec library gives me the following error:
No type class instance was found for
Data.Argonaut.Encode.Class.EncodeJson LessonType
while solving type class constraint
Data.Argonaut.Encode.Class.GEncodeJson ( children :: Array
{ elements :: { audio :: String
, image :: String
, translation :: String
, vocabText :: String
}
, layout :: String
, node_level :: String
}
, lesson_type :: LessonType
, node_level :: String
)
(Cons @Type "lesson_type" LessonType (Cons @Type "node_level" String (Nil @Type)))
while checking that type forall (@a :: Type). EncodeJson a => a -> Json
is at least as general as type Array
{ children :: Array ...
, description :: String
, name :: String
, node_level :: String
, theme_image_path :: String
}
-> Json
while checking that expression encodeJson
has type Array
{ children :: Array ...
, description :: String
, name :: String
, node_level :: String
, theme_image_path :: String
}
-> Json
in value declaration contentToJson
The second also works, although it is verbose. Unfortunately, I run into a similar error as before:
No type class instance was found for Data.Argonaut.Encode.Class.EncodeJson NodeLevel ...
2. Variable sub-schemas
In TS, I would do something like this:
type Lesson = {
node_level: NodeLevel;
children: Activity[];
lesson_type: LessonType;
}
type ActivityName =
"vocab"
| "grammar"
| (... etc)
type Activity =
Vocabulary
| Grammar
| (... etc)
type Vocabulary = {
node_level: NodeLevel,
activity_type: ActivityName,
elements: {
audio: string,
image: string,
vocabText: string,
translation: string
},
}
-- etc. for more Activity options
This ensures that the array of Activity does indeed contain specifically this schema. How to do this in Purescript? Purescript doesn't seem to accept types being one of several possibilities using the | operator, unless you use data (is that what I should be doing? How? It seems it would run into the error from point 1 above).
3. Enforced values
My JSON should ideally have specific values of node_level depending on the sub-schema / level of the hierarchy, e.g. Vocabulary should have node_level: "activity" enforced.