No longer actively maintained, please check out Felicity
FSharp.JsonApi is a library (not framework) that allows you to use F# to easily create and consume flexible, strongly typed web APIs following the JSON:API specification. There’s even an almost-production-ready API implementation sample to get you started on the right foot!
Core features:
- Full support for sparse fieldsets and included resources
- Support for loading included resources asynchronously on-demand, in parallel
- Uses FSharp.JsonSkippable for strong typing of whether JSON properties are included or excluded
- Plays very nicely with the robust error handling of FsToolkit.ErrorHandling, whether monadic or applicative (the latter works perfectly to return multiple JSON:API errors at once)
- Lots of utilities to allow you to easily wire up your perfect domain snowflakes (single-case DU wrappers with smart constructors,
Result
-returning record field setters, etc.) with the raw resource attributes and relationships present in a JSON:API request - And much more
The focus is on server implementations, but it may also be useful when implementing clients (please get in touch!).
We use this library for at least three mission-critical production APIs. I have developed and tweaked it internally for around one and a half years before finally polishing it and publishing to NuGet. I’m not claiming it’s perfect, or even bug-free, but it’s battle-tested, and I have a vested interest in keeping this library working properly.
FSharp.JsonApi consists of three NuGet packages:
- FSharp.JsonApi contains all the core stuff: JSON:API document models for serialization/deserialization, resource builders, parsing and validation of query parameters and documents, helpers for calling domain code based on a JSON:API request, etc. If you don’t use ASP.NET Core, you can easily use this library to build your own abstractions.
- FSharp.JsonApi.AspNetCore contains lots of useful helpers and additional overloads for parsing and validating requests using ASP.NET Core’s
HttpContext
. - FSharp.JsonApi.Giraffe contains a few simple
HttpHandler
s that may be useful if using Giraffe.
Install all packages that are relevant for you. You can get away with installing only the highest-level package – e.g. FSharp.JsonApi.Giraffe – and have the rest installed automatically as transitive dependencies, but depending on your package manager, you might not be able to easily update the lower-level (transitive) packages (Paket is better at this than NuGet).
Contributions and ideas are welcome! Please see Contributing.md for details.
I highly recommend you check out the sample API in this repo, which is a simple but complete and almost-production-ready example API implementation. Open the main solution in VS, start at the topmost file, and read through the project in compilation order. There are lots of comments along the way to explain what’s going on.
As a very short introduction, I hope the steps below are useful, but bear in mind that they only skim the surface.
Define each resource’s attributes and relationships using records, enums, and any relevant .NET attributes. The names will be used as-is; use double backticks for special names. Use option
for nullable attributes. Use ToOne
and ToMany
for relationships. All attributes and relationships must be wrapped in Skippable
(see FSharp.JsonSkippable).
type ArticleType =
| personal = 0
| commercial = 1
[<CLIMutable>]
type ArticleAttrs = {
title: string Skippable
articleType: ArticleType Skippable
[<ReadOnly>] updated: DateTimeOffset option Skippable
}
[<CLIMutable>]
type ArticleRels = {
[<NotNull; AllowedTypes("person")>]
author: ToOne Skippable
[<ReadOnly; AllowedTypes("comment")>]
comments: ToMany Skippable
}
The resource discriminator is a DU where each case is a Resource<'attrs, 'rels>
. It’s used to provide FSharp.JsonApi with all relevant information about the resources, and allows for a nice and friendly syntax when parsing resources.
type ResourceDiscriminator =
| [<ResourceName("article")>]
Article of Resource<ArticleAttrs, ArticleRels>
The JsonApiContext
is what actually contains all information about the resources, obtained from the resource discriminator. It is used to serialize, deserialize, create, parse, and validate documents. Define it once and use it everywhere.
let jsonApiCtx = JsonApiContext.create<ResourceDiscriminator>
The ResourceBuildContext
is your friend when building attributes, relationships, links, and meta. It contains all information about sparse fieldsets, the current resource being built, its place in the include path, etc. and provides many helpers to get attributes, relationships, and related resources. Most ResourceBuildContext
methods have many overloads for convenience; only some are used below.
module Article =
let private getIdentifier (a: Article) =
ResourceIdentifier.create TypeNames.article a.Id
let private getAttributes (ctx: ResourceBuildContext) (a: Article) =
{
title = ctx.GetAttribute("title", a.Title)
articleType = ctx.GetAttribute("articleType", a.Type, ArticleType.toApi)
updated = ctx.GetAttribute("updated", a.Updated)
}
let private getRelationships baseUrl (ctx: ResourceBuildContext) (a: Article) =
async {
// Use Async.StartChild to fetch the included resources in parallel
let! authorComp =
ctx.IncludeToOne("author", a.Id, Db.Person.authorForArticle, Person.getBuilder baseUrl)
|> Async.StartChild
let! commentsComp =
ctx.IncludeToMany("comments", a.Id, Db.Comment.allForArticle, Comment.getBuilder baseUrl)
|> Async.StartChild
// Actually wait for them to be fetched
let! authorRelationship, authorBuilder = authorComp
let! commentsRelationship, commentsBuilders = commentsComp
// Return the relationships and the builders that will be
// used to build the related resources
let relationships = {
author = authorRelationship
comments = commentsRelationship
}
let builders = [
yield! authorBuilder |> Option.toList
yield! commentsBuilders
]
return relationships, builders
}
// Here is the function to actually get the resource builder.
// It must be cheap (do not eagerly evaluate attributes or resources)
// and the two final parameters must be the ResourceBuildContext
// and the domain object to be built.
let getBuilder baseUrl (ctx: ResourceBuildContext) a =
ResourceBuilder
.Create(Article, getIdentifier a)
.WithAttributes(fun () -> getAttributes ctx a)
.WithRelationships(getRelationships baseUrl ctx a)
// You can add links and meta, too; check out the sample API
You can now build JSON:API documents like so:
jsonApiCtx.BuildDocument(article, Article.getBuilder, ctx)
where ctx
is the ASP.NET Core HttpContext
. Your resource document will be built and all sparse fieldsets, included resources etc. will be handled automatically for you.
For example:
let result =
jsonApiCtx
.WithNoIdForPost()
.Parse(Article, ctx)
This returns Async<Result<Resource<ArticleAttrs, ArticleRels>, RequestDocumentError list>>
. See the sample API for more information on simple and robust error handling, and more deserialization/parsing/validation options.
Example of applicative error handling (using operators from FsToolkit.ErrorHandling):
ArticleSearchArgs.create
<!> Query.GetSingle("filter[title]", ctx)
<*> Query.GetBoundInt("page[offset]", 0, ctx, min=0)
<*> Query.GetBoundInt("page[limit]", 10, ctx, min=1)
result {
let (a: ArticleAttrs) = Resource.attributesOrDefault result
let! article =
Ok article
|> set.Optional(Article.setTitle, Attribute.Get(a.title))
|> set.Required(Article.setBody, Attribute.Require(a.body))
}
Many functions in FSharp.JsonApi return Result<_, SomeError list>
where SomeError
is one of a few discriminated union types having cases that contain all the information you need to provide useful errors to the client in whatever manner you desire.
You can, for example, define your own DU for all errors returned through your API, and map FSharp.JsonApi’s errors to this type. You can then have a function that takes your error DU and returns a JSON:API error object. This allows you to easily take an FSharp.JsonApi error (or a list of errors) and produce a JSON:API error response. See ErrorHandling.fs
as well as the error-related functions in the first part of HttpHandlers.fs
in the sample API. (I basically copy-paste all of that whenever I create a new JSON:API.)
Nulls are only allowed for option
-wrapped properties. Any other null
in the response body will make JsonApiContext.Parse
return errors you can use in an error response as described above.
You can also easily require that option
-wrapped attributes be non-null
for a specific request, giving you a syntactically concise way to get either the inner value of an Option
or an error you can return to the API client. It can, for example, be easily chained along with other setters as shown above.
Check out the sample API y’all 😉
It would be nice to have complete API documentation, but I have no immediate plans to work on that.
In the meantime, I highly recommend you check out the sample API in this repo, which is a simple but complete and almost-production-ready example API implementation. Open the main solution in VS, start at the topmost file, and read through the project in compilation order. There are lots of comments along the way to explain what’s going on.
- Improved
Attribute
overload resolution
- Added
Attribute
overloads for async parsers, plus a few more - Added
Setter
overloads accepting async arguments
- Fixed overload resolution for
Async<'a>
overloads ofResourceBuildContext
methodsGetAttribute
andGetExplicitAttribute
- Breaking: Removed
SimpleResource
and related methods/extensions onJsonApiContext
. UseResource.attributesOrDefault
andResource.relationshipsOrDefault
instead to get a (possibly default) attribute/relationship instance from a resource. - Breaking: Added
RequestDocumentError.UnknownMainResourceType
- Breaking:
JsonApiContext.GetResource
and theJsonApiContext.Parse
overload returning a resource discriminator now return errors if the resource type is unknown. The signature of the former method is changed; the latter is a behaviour change only. - Breaking: Renamed
Query
toQueryParser
, added static methods to create an instance with a query parameter map orHttpContext
, and changed most methods to instance members that do not depend on a query parameter map orHttpContext
- Breaking:
JsonApiContext.create
now throws on invalid attribute and relationship names. Use the newAllowIllegalNameAttribute
to disable the check for specific attributes/relationships. - Added
Setter
type with helper methods to chain and lift normal "immutable setter" functions to accept parsed, possibly optional arguments, combining any errors - Added
JsonApiContext
methodsRequireResource
,WithAllReadOnly
,ToDiscriminator
,FromDiscriminator
, andSerializeAndGetBytes
- Added
JsonApiContext
overloads forParseRequired
,WithReadOnly
,WithWriteOnly
, andWithNotNull
- Added async overloads for
ResourceBuildContext
methodsGetAttribute
andGetExplicitAttribute
- Added an
HttpContext.WriteJsonApiAsync
extension overload accepting a byte array - Added new Giraffe HTTP handlers
jsonApiETag
,jsonApiETagWith
, andjsonApiBytes
- Fixed
Uri.addQuery
andUri.setQuery
not behaving correctly for multiple identical query keys and for query keys that only differ by case - Fixed validation when
links
collection isnull
(JSON:"links": null
) - Fixed validation when a relationship is null (JSON:
"relationships": {"myRel": null}
) - Serializes
Uri
s to canonical format usingUri.ToString()
instead ofUri.OriginalString
which is normally used by Newtonsoft.Json. See dotnet/corefx#41679 and JamesNK/Newtonsoft.Json/2190. - Made order of included resources deterministic (needed to get stable hashes of response for ETag)
- Fixed
Attribute.GetNonNull
returning errors for skipped values
- Add
validate
optional parameter to allJsonApiContext.Parse
extensions - Add
ParseRequired
andParseSimpleRequired
extensions members forJsonApiContext
- Add
ParseRequired
andParseSimpleRequired
toJsonApiContext
- Add more
Attribute
overloads
- Add more
Relationship
overloads
- Initial release