JsonApiKit.SourceGenerators 1.0.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JsonApiKit.SourceGenerators" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JsonApiKit.SourceGenerators" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="JsonApiKit.SourceGenerators" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JsonApiKit.SourceGenerators --version 1.0.0
                    
#r "nuget: JsonApiKit.SourceGenerators, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JsonApiKit.SourceGenerators@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JsonApiKit.SourceGenerators&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=JsonApiKit.SourceGenerators&version=1.0.0
                    
Install as a Cake Tool

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

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 attributes
  • IJsonApiResource<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.

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 Author and Book entities
  • IQueryableEntity<T> implementations with filter, sort, and include
  • Resource constructors with IncludeContext depth tracking
  • ToPagedResponseAsync query pipeline
  • Exception handler and result extensions

Run it:

cd samples/JsonApiKit.Sample
dotnet run

Then visit:

  • GET /api/v1/books — list books with pagination
  • GET /api/v1/books?include=author — include author relationship
  • GET /api/v1/books/1 — single book
  • GET /api/v1/authors — list authors
  • GET /api/v1/error — exception handler demo

License

MIT — Copyright (c) 2026 JsonApiKit Contributors

There are no supported framework assets in this package.

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