Hi there,
As the title implies I’m a bit stuck on how to best model APIs that support creating objects (with a certain shape of optional vs non-optional fields), getting objects (with another shape of optional vs non-optional fields) and patching objects (where nearly all fields are optional) and being able to re-use as much of the types/codecs as possible.
We’ve settled on Jzon to do our JSON (de)serialisation for now but are a bit frustrated with our current implementation that only really allows for the decoding side of the defined codec.
At our company we use Feathers which has the concept of services which provide a standard way to create and query resources, below is my attempt at binding to these services.
module type ServiceSpec = {
type item
type patch
type create
type query
let codec: Jzon.codec<item>
let path: string
}
module MakeService = (Service: ServiceSpec) => {
// bindings to our client library
}
The only type that is required to be well-defined is the item
type as you have to write a decoder for it, the patch
and create
type can be whatever makes sense for that service, in general both will be @deriving(abstract)
types with a number of @optional
decorated fields. This has been working for us so far up until we have nested rescript types, e.g.
module SomeServiceSpec = {
type nested = Attribute(string)
type item = {
id: string,
nested: array<nested>,
}
let nestedCodec = Jzon.object1(
nested =>
switch nested {
| Attribute(attribute) => attribute
},
attribute =>
switch attribute {
| "value1"
| "value2"
| "value3" =>
Attribute(attribute)->Ok
| _ =>
Error(#UnexpectedJsonValue([Jzon.DecodingError.Field("attribute")], "Invalid attribute"))
},
Jzon.field("attribute", Jzon.string),
)
let codec = Jzon.object2(
obj => (
obj.id,
obj.nested,
),
((
id,
nested,
)) =>
{
id: id,
nested: nested,
}->Ok,
Jzon.field("id", Jzon.string)->Jzon.optional,
Jzon.field("nested", Jzon.array(nestedCodec))->Jzon.default([]),
)
@deriving(abstract)
type patch = {
@optional nested: array<{"attribute": string}>,
}
@deriving(abstract)
type create = {
nested: array<{"attribute": string}>,
}
let path = "..."
}
In this case the nested
field of the item is a variant. This means that the patch
(and create
) type need to be defined differently as array<{"attribute": string}>
and need to be translated from the variant at the call site of patch
and create
each time, when we could be using the coder part of the codec. If we do that, we need to change the type of nested
to Js.Json.t
which means anything could go in there and we lose a bit of safety.
Is there a way to create a version of the Js.Json.t type that requires the caller to use a certain codec to create it?
Or alternatively, I imagine every web project would come across this problem, is there just a better way of approaching this?
Thanks