JsonApiKit.SourceGenerators
1.0.0
See the version list below for details.
dotnet add package JsonApiKit.SourceGenerators --version 1.0.0
NuGet\Install-Package JsonApiKit.SourceGenerators -Version 1.0.0
<PackageReference Include="JsonApiKit.SourceGenerators" Version="1.0.0" />
<PackageVersion Include="JsonApiKit.SourceGenerators" Version="1.0.0" />
<PackageReference Include="JsonApiKit.SourceGenerators" />
paket add JsonApiKit.SourceGenerators --version 1.0.0
#r "nuget: JsonApiKit.SourceGenerators, 1.0.0"
#:package JsonApiKit.SourceGenerators@1.0.0
#addin nuget:?package=JsonApiKit.SourceGenerators&version=1.0.0
#tool nuget:?package=JsonApiKit.SourceGenerators&version=1.0.0
JsonApiKit
Lightweight JSON:API library for .NET — drop-in resource shapes, pagination, error handling, and query parsing without owning your app architecture.
JsonApiKit gives you the building blocks to produce JSON:API-compliant (or relaxed) responses from any .NET API. It does not take over your routing, controllers, or data access — you stay in control while getting consistent, well-structured output.
Table of Contents
- Packages
- Installation
- Quick Start
- Core Concepts
- Configuration
- Include Depth Tracking
- Sparse Fieldsets
- ASP.NET Core Integration
- EF Core Integration
- Source Generators
- JSON Output Examples
- Sample Application
- License
Packages
| Package | Description | Target |
|---|---|---|
| JsonApiKit | Core types — documents, resources, pagination, errors, serialization. Zero ASP.NET dependency. | netstandard2.1, net8.0, net9.0 |
| JsonApiKit.AspNetCore | ASP.NET Core integration — result extensions, exception handling, validation filters, DI registration. | net8.0 |
| JsonApiKit.EntityFrameworkCore | EF Core integration — query pipeline, cursor/offset pagination, paged responses. | net8.0 |
| JsonApiKit.SourceGenerators | Roslyn source generators — reduce resource boilerplate with attributes. | netstandard2.0 |
Installation
# Core (required)
dotnet add package JsonApiKit
# ASP.NET Core integration
dotnet add package JsonApiKit.AspNetCore
# EF Core query pipeline
dotnet add package JsonApiKit.EntityFrameworkCore
# Source generators (optional)
dotnet add package JsonApiKit.SourceGenerators
Quick Start
using JsonApiKit.AspNetCore.DependencyInjection;
using JsonApiKit.AspNetCore.Results;
using JsonApiKit.Configuration;
using JsonApiKit.Includes;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddJsonApiKit(options =>
{
options.Mode = JsonApiMode.Relaxed;
options.DefaultPageSize = 10;
options.MaxPageSize = 50;
options.DefaultPaginationType = PaginationType.Cursor;
options.IncludeExceptionDetails = builder.Environment.IsDevelopment();
});
var app = builder.Build();
app.UseExceptionHandler(o => { });
app.MapGet("/api/v1/books/{id:int}", async (int id, MyDbContext db, string? include) =>
{
var includeCtx = IncludeParser.Parse(include);
var book = await db.Books.FindAsync(id);
if (book is null)
return "Book not found".ToJsonApiError("NOT_FOUND", 404);
return new BookResource(book, includeCtx).ToJsonApiResult("BOOK_FETCHED");
});
app.Run();
Core Concepts
Documents
All responses are wrapped in a document type:
// Success response
JsonApiDocument<TData>
{
ResponseCode, // application-level code, e.g. "BOOKS_FETCHED"
ResponseMessage, // human-readable message
HttpCode, // HTTP status code
Data, // the payload
Errors, // optional error list
Meta, // optional metadata dictionary
Links, // optional links
Included // optional sideloaded resources
}
// Error response
JsonApiErrorDocument
{
ResponseCode,
ResponseMessage,
HttpCode,
Errors, // required error list
Meta
}
Resources
Implement IJsonApiResource to define a resource shape:
public class BookResource : IJsonApiResource
{
public string Id { get; set; } = "";
public string Type => "book";
public BookAttributes Attributes { get; set; } = null!;
public BookRelationships? Relationships { get; set; }
public BookResource(Book book, IncludeContext ctx)
{
Id = book.Id.ToString();
Attributes = new BookAttributes(book.Title, book.Slug);
Relationships = ctx.ShouldInclude("author") && book.Author is not null
? new BookRelationships(new AuthorResource(book.Author, ctx.Descend("author")))
: null;
}
}
public record BookAttributes(string Title, string Slug);
public record BookRelationships(AuthorResource? Author);
Typed variants are also available:
IJsonApiResource<TAttributes>— resource with typed attributesIJsonApiResource<TAttributes, TRelationships>— resource with typed attributes and relationships
Relationships
For strict-mode relationship serialization:
// To-one: serializes as { "data": { "id": "1", "type": "author" } }
Relationship<AuthorResource>
// To-many: serializes as { "data": [{ "id": "1", "type": "book" }, ...] }
RelationshipCollection<BookResource>
// Resource identifier
ResourceIdentifier(string Id, string Type)
// Strict relationship records
RelationshipToOne(ResourceIdentifier? Data)
RelationshipToMany(IReadOnlyList<ResourceIdentifier>? Data)
The IncludedCollector deduplicates resources by (Type, Id) for the top-level included array.
Errors
public record JsonApiError
{
public required string Code { get; init; } // e.g. "VALIDATION_ERROR"
public string? Detail { get; init; } // human-readable detail
public string? Status { get; init; } // HTTP status as string
public JsonApiErrorSource? Source { get; init; } // pointer + parameter
public JsonApiMeta? Meta { get; init; }
}
public record JsonApiErrorSource
{
public string? Pointer { get; init; } // e.g. "/data/attributes/title"
public string? Parameter { get; init; } // e.g. "filter"
}
Pagination
Two pagination strategies with polymorphic metadata:
Offset pagination:
PaginationMeta.Offset(new OffsetPage(
Current: 1, Size: 10, Total: 42, TotalPages: 5,
HasNext: true, HasPrevious: false, From: 1, To: 10
))
Cursor pagination:
PaginationMeta.Cursor(new CursorPage(
Size: 10,
Next: "base64-encoded-cursor",
Previous: null
))
PaginationCursor<TId> provides Base64URL-encoded cursors containing timestamp + ID for stable, keyset-based pagination.
Links and Meta
ResourceLinks(string Self) // per-resource self link
PaginationLinks(string? First, string? Last, // page navigation
string? Prev, string? Next)
JsonApiLinks { Self, Related, Pagination } // document-level links
JsonApiMeta : Dictionary<string, object?> // arbitrary metadata
Configuration
builder.Services.AddJsonApiKit(options =>
{
options.Mode = JsonApiMode.Relaxed; // Relaxed (default) or Strict
options.NamingPolicy = JsonNamingPolicy.CamelCase;
options.DefaultPageSize = 10;
options.MaxPageSize = 100;
options.DefaultPaginationType = PaginationType.Cursor;
options.IncludeLinks = false;
options.BaseUrl = "https://api.example.com";
options.IncludeExceptionDetails = false; // true in dev for stack traces
});
Relaxed vs Strict Mode
Relaxed mode (default) — relationships are serialized as full resource objects inline:
{
"data": {
"id": "1",
"type": "book",
"attributes": { "title": "1984" },
"relationships": {
"author": { "id": "1", "type": "author", "attributes": { "name": "George Orwell" } }
}
}
}
Strict mode — follows the JSON:API specification exactly. Relationships contain only resource linkage; full resources go into a top-level included array:
{
"data": {
"id": "1",
"type": "book",
"attributes": { "title": "1984" },
"relationships": {
"author": { "data": { "id": "1", "type": "author" } }
}
},
"included": [
{ "id": "1", "type": "author", "attributes": { "name": "George Orwell" } }
]
}
Include Depth Tracking
IncludeContext is the key mechanism for controlling which relationships get loaded and how deep the inclusion chain goes. It prevents N+1 issues and circular reference bloat.
// Parse the ?include= query parameter
var ctx = IncludeParser.Parse("author,author.books", maxAllowedDepth: 3);
// Check if a relationship should be included at the current level
ctx.ShouldInclude("author"); // true
// Descend into a relationship (increments depth, updates path)
var authorCtx = ctx.Descend("author");
authorCtx.ShouldInclude("books"); // true (matches "author.books")
// Further nesting respects max depth
var booksCtx = authorCtx.Descend("books");
booksCtx.ShouldInclude("author"); // false (depth limit reached)
Usage in resource constructors:
public BookResource(Book book, IncludeContext ctx)
{
Id = book.Id.ToString();
Attributes = new BookAttributes(book.Title, book.Slug);
if (ctx.ShouldInclude("author") && book.Author is not null)
Relationships = new BookRelationships(
new AuthorResource(book.Author, ctx.Descend("author")));
}
IncludeParser.Parse() is case-insensitive, supports comma-separated dot-notation paths, and clamps to a configurable maximum depth (default 3).
Sparse Fieldsets
SparseFieldsetContext parses the fields[type]=field1,field2 query string pattern:
var fieldsetCtx = SparseFieldsetContext.Parse(httpContext.Request.QueryString.Value);
// Check if a specific field should be included for a type
fieldsetCtx.ShouldIncludeField("book", "title"); // true or false
// Get all requested fields for a type
HashSet<string>? fields = fieldsetCtx.GetFieldsFor("book");
Sparse fieldsets are not enforced at the serialization layer — they are designed to be checked during resource mapping so you have full control over field filtering.
ASP.NET Core Integration
DI Registration
builder.Services.AddJsonApiKit(options =>
{
options.Mode = JsonApiMode.Relaxed;
// ...
});
This registers IOptions<JsonApiOptions>, a configured JsonSerializerOptions singleton, and JsonApiExceptionHandler.
Result Extensions
Convert values directly to JSON:API responses:
// Success
var resource = new BookResource(book, includeCtx);
return resource.ToJsonApiResult("BOOK_FETCHED");
return resource.ToJsonApiResult("BOOK_CREATED", "Book created successfully", 201);
// Error from string
return "Book not found".ToJsonApiError("NOT_FOUND", 404);
return "Unauthorized".ToJsonApiError("UNAUTHORIZED", 401, "You must be logged in");
// Validation errors
return errors.ToJsonApiValidationError(validationDictionary);
Exception Handler
Unhandled exceptions are automatically mapped to JsonApiErrorDocument:
| Exception | HTTP Status | Code |
|---|---|---|
KeyNotFoundException |
404 | NOT_FOUND |
ArgumentException |
400 | BAD_REQUEST |
UnauthorizedAccessException |
401 | UNAUTHORIZED |
NotSupportedException |
405 | METHOD_NOT_ALLOWED |
| Other | 500 | INTERNAL_ERROR |
Stack traces are included when IncludeExceptionDetails = true (recommended for development only).
app.UseExceptionHandler(o => { });
Validation Filter
Add the endpoint filter to validate request bodies:
app.MapPost("/api/v1/books", (CreateBookRequest request) => { ... })
.AddEndpointFilter<JsonApiValidationFilter>();
Validators implement IJsonApiValidator<T> and produce field-level errors with JSON:API source pointers:
{
"responseCode": "VALIDATION_ERROR",
"httpCode": 422,
"errors": [
{
"code": "VALIDATION_ERROR",
"detail": "Title is required",
"status": "422",
"source": { "pointer": "/data/attributes/title" }
}
]
}
EF Core Integration
IQueryableEntity
Implement IQueryableEntity<T> on your entities to define filter, sort, and include logic as static methods:
public class Book : IQueryableEntity<Book>
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int AuthorId { get; set; }
public Author Author { get; set; } = null!;
public DateTime CreatedAt { get; set; }
public static IQueryable<Book> ApplyFilter(IQueryable<Book> query, object? filter)
{
if (filter is string search && !string.IsNullOrWhiteSpace(search))
return query.Where(b => b.Title.Contains(search));
return query;
}
public static IQueryable<Book> ApplySort(IQueryable<Book> query, string? sort) => sort switch
{
"title" => query.OrderBy(b => b.Title).ThenBy(b => b.Id),
"-title" => query.OrderByDescending(b => b.Title).ThenByDescending(b => b.Id),
_ => query.OrderByDescending(b => b.CreatedAt).ThenByDescending(b => b.Id)
};
public static IQueryable<Book> ApplyInclude(
IQueryable<Book> query, object? include, IncludeContext context)
{
if (context.ShouldInclude("author"))
query = query.Include(b => b.Author);
return query;
}
}
Paged Responses
ToPagedResponseAsync chains filter → sort → include → paginate → map in one call:
var includeCtx = IncludeParser.Parse(include);
var request = new ApiRequest<string?, string?, object>
{
Include = include,
Filter = filter,
Sort = sort,
Page = new RequestPage { Size = pageSize, Number = pageNumber }
};
var result = await db.Books
.ToPagedResponseAsync<Book, string?, string?, object, BookResource>(
request,
(entity, ctx) => new BookResource(entity, ctx),
includeCtx);
return result.ToJsonApiResult("BOOKS_FETCHED");
Supports both offset pagination (pageNumber/pageSize) and cursor pagination with PaginationCursor<TId>.
Source Generators
Reduce boilerplate with the [JsonApiResource] attribute:
[JsonApiResource("book", "/api/v1/books")]
public partial class BookResource
{
public string Id { get; set; }
[JsonApiAttribute]
public string Title { get; set; }
[JsonApiRelationship("author")]
public AuthorResource? Author { get; set; }
}
The generator produces a partial class with the Type property and Links based on the resource type and base path.
Attributes are emitted via RegisterPostInitializationOutput, so consuming projects only need the analyzer reference — no additional package dependency.
JSON Output Examples
Success (relaxed mode):
{
"responseCode": "BOOKS_FETCHED",
"responseMessage": "Success",
"httpCode": 200,
"data": [
{
"id": "1",
"type": "book",
"attributes": {
"title": "1984",
"slug": "1984"
},
"relationships": {
"author": {
"id": "1",
"type": "author",
"attributes": {
"name": "George Orwell",
"slug": "george-orwell"
}
}
}
}
],
"meta": {
"paginationType": "cursor",
"page": {
"size": 10,
"next": "eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTAxIiwiaWQiOjF9"
}
}
}
Error:
{
"responseCode": "NOT_FOUND",
"responseMessage": "Book not found",
"httpCode": 404,
"errors": [
{
"code": "NOT_FOUND",
"detail": "Book not found",
"status": "404"
}
]
}
Sample Application
A complete working example is included at samples/JsonApiKit.Sample/. It demonstrates:
- SQLite database with
AuthorandBookentities IQueryableEntity<T>implementations with filter, sort, and include- Resource constructors with
IncludeContextdepth tracking ToPagedResponseAsyncquery pipeline- Exception handler and result extensions
Run it:
cd samples/JsonApiKit.Sample
dotnet run
Then visit:
GET /api/v1/books— list books with paginationGET /api/v1/books?include=author— include author relationshipGET /api/v1/books/1— single bookGET /api/v1/authors— list authorsGET /api/v1/error— exception handler demo
License
MIT — Copyright (c) 2026 JsonApiKit Contributors
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.4.0 | 156 | 4/1/2026 |
| 1.3.13 | 212 | 3/26/2026 |
| 1.3.11 | 100 | 3/26/2026 |
| 1.3.9 | 102 | 3/25/2026 |
| 1.3.8 | 107 | 3/25/2026 |
| 1.3.7 | 99 | 3/25/2026 |
| 1.3.6 | 115 | 3/7/2026 |
| 1.3.5 | 100 | 3/7/2026 |
| 1.3.4 | 101 | 3/7/2026 |
| 1.3.3 | 108 | 3/7/2026 |
| 1.3.2 | 105 | 3/5/2026 |
| 1.3.1 | 107 | 3/5/2026 |
| 1.3.0 | 112 | 3/5/2026 |
| 1.2.2 | 107 | 2/27/2026 |
| 1.2.1 | 106 | 2/26/2026 |
| 1.2.0 | 105 | 2/26/2026 |
| 1.1.0 | 110 | 2/26/2026 |
| 1.0.0 | 114 | 2/23/2026 |