Crews.Web.JsonApiClient 6.0.0

Prefix Reserved
dotnet add package Crews.Web.JsonApiClient --version 6.0.0
                    
NuGet\Install-Package Crews.Web.JsonApiClient -Version 6.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="Crews.Web.JsonApiClient" Version="6.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Crews.Web.JsonApiClient" Version="6.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Crews.Web.JsonApiClient" />
                    
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 Crews.Web.JsonApiClient --version 6.0.0
                    
#r "nuget: Crews.Web.JsonApiClient, 6.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 Crews.Web.JsonApiClient@6.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=Crews.Web.JsonApiClient&version=6.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Crews.Web.JsonApiClient&version=6.0.0
                    
Install as a Cake Tool

JsonApiClient

A .NET client library for deserializing JSON:API v1.1 documents into strongly-typed C# objects.

Installation

JsonApiClient is available as a NuGet package:

dotnet add package Crews.Web.JsonApiClient

Quick Start

// Step 1: Define your base model
public class Article
{
    public string? Title { get; init; }
    public string? Body { get; init; }
    public DateTime? PublishedAt { get; init; }
}

// Step 2: Define a strongly-typed resource class extending JsonApiResource<T>
public class ArticleResource : JsonApiResource<Article> { }

// Step 3: Deserialize using the static Deserialize() method
string json = /* your JSON:API document */;
var document = JsonApiDocument<ArticleResource>.Deserialize(json);

// Step 4: Access strongly-typed data with full IntelliSense support!
if (document.HasErrors)
{
    foreach (var error in document.Errors)
    {
        Console.WriteLine($"Error {error.Status}: {error.Title}");
    }
}
else if (document.Data != null)
{
    // Data is strongly-typed as Article - get full IntelliSense!
    Console.WriteLine($"Title: {document.Data.Attributes?.Title}");
    Console.WriteLine($"Published: {document.Data.Attributes?.PublishedAt}");

    // Access typed relationships
    var authorId = document.Data.Relationships?.Author?.Data?.Id;
    Console.WriteLine($"Author ID: {authorId}");
}

// For collection documents, use JsonApiCollectionDocument<T>
var collection = JsonApiCollectionDocument<ArticleResource>.Deserialize(json);

if (collection.Data != null)
{
    foreach (var article in collection.Data)
    {
        Console.WriteLine($"Article: {article.Attributes?.Title}");
    }
}

Weakly-Typed Deserialization (For Dynamic Schemas)

If you're working with dynamic or unknown schemas, you can use the weakly-typed base classes:

// Deserialize without custom types
string json = /* your JSON:API document */;
var document = JsonApiDocument.Deserialize(json);

// Check what type of document you have
if (document.HasErrors)
{
    foreach (var error in document.Errors)
    {
        Console.WriteLine($"Error {error.Status}: {error.Title}");
    }
}
else if (document.HasCollectionResource)
{
    // Manually deserialize the Data property
    var resources = document.Data?.Deserialize<List<JsonApiResource>>();
    Console.WriteLine($"Found {resources?.Count} resources");
}
else
{
    // Single resource - manually deserialize the Data property
    var resource = document.Data?.Deserialize<JsonApiResource>();
    Console.WriteLine($"Resource: {resource?.Type} with ID {resource?.Id}");

    // Access attributes dynamically
    var title = resource?.Attributes?["title"]?.GetValue<string>();
    Console.WriteLine($"Title: {title}");
}

Complete Real-World Example

Here's a complete example showing how to define and use strongly-typed resources:

using System.Text.Json;
using System.Text.Json.Serialization;
using Crews.Web.JsonApiClient;

// Define your resource types
public class UserResource : JsonApiResource<User, UserRelationships> { }

public class User
{
    [JsonPropertyName("name")]
    public string? Name { get; init; }

    [JsonPropertyName("email")]
    public string? Email { get; init; }

    [JsonPropertyName("createdAt")]
    public DateTime? CreatedAt { get; init; }
}

public class UserRelationships
{
    [JsonPropertyName("posts")]
    public JsonApiCollectionRelationship<JsonApiResource<Post>>? Posts { get; init; }

    [JsonPropertyName("profile")]
    public JsonApiRelationship<JsonApiResource<Profile>>? Profile { get; init; }
}

// Use the types
string json = """
{
  "data": {
    "type": "users",
    "id": "123",
    "attributes": {
      "name": "John Doe",
      "email": "john@example.com",
      "createdAt": "2024-01-15T10:30:00Z"
    },
    "relationships": {
      "posts": {
        "data": [
          { "type": "posts", "id": "1" },
          { "type": "posts", "id": "2" }
        ]
      },
      "profile": {
        "data": { "type": "profiles", "id": "456" }
      }
    }
  }
}
""";

var document = JsonApiDocument<UserResource>.Deserialize(json);

// Access with full type safety and IntelliSense
if (document.Data != null)
{
    Console.WriteLine($"User: {document.Data.Attributes?.Name}");
    Console.WriteLine($"Email: {document.Data.Attributes?.Email}");
    Console.WriteLine($"Created: {document.Data.Attributes?.CreatedAt}");

    // Access typed relationships
    var posts = document.Data.Relationships?.Posts?.Data;
    Console.WriteLine($"Number of posts: {posts?.Count ?? 0}");

    var profileId = document.Data.Relationships?.Profile?.Data?.Id;
    Console.WriteLine($"Profile ID: {profileId}");
}

Working with Resources

// Use strongly-typed document with custom resource class
var document = JsonApiDocument<ArticleResource>.Deserialize(json);

// Access resource identification
Console.WriteLine($"Type: {document.Data?.Type}");
Console.WriteLine($"ID: {document.Data?.Id}");

// Access strongly-typed attributes with IntelliSense
if (document.Data?.Attributes != null)
{
    var title = document.Data.Attributes.Title;  // Full IntelliSense!
    var publishedAt = document.Data.Attributes.PublishedAt;  // Strongly-typed!
    Console.WriteLine($"{title} published at {publishedAt}");
}

// Access metadata (flexible JSON object for extension data)
if (document.Data?.Meta != null)
{
    var copyright = document.Data.Meta["copyright"]?.GetValue<string>();
    Console.WriteLine($"Copyright: {copyright}");
}

// Navigate links
if (document.Data?.Links?["self"] != null)
{
    Console.WriteLine($"Self link: {document.Data.Links["self"].Href}");
}
Weakly-Typed Approach (For Dynamic Schemas)
// Deserialize manually from Data property
var document = JsonApiDocument.Deserialize(json);
var resource = document.Data?.Deserialize<JsonApiResource>();

// Access resource identification
Console.WriteLine($"Type: {resource?.Type}");
Console.WriteLine($"ID: {resource?.Id}");

// Access attributes (flexible JSON object)
if (resource?.Attributes != null)
{
    var title = resource.Attributes["title"]?.GetValue<string>();
    var publishedAt = resource.Attributes["publishedAt"]?.GetValue<DateTime>();
    Console.WriteLine($"{title} published at {publishedAt}");
}

// Access metadata
if (resource?.Meta != null)
{
    var copyright = resource.Meta["copyright"]?.GetValue<string>();
    Console.WriteLine($"Copyright: {copyright}");
}

// Navigate links
if (resource?.Links?["self"] != null)
{
    Console.WriteLine($"Self link: {resource.Links["self"].Href}");
}

Working with Relationships

var document = JsonApiDocument<ArticleResource>.Deserialize(json);

// Access strongly-typed relationships with IntelliSense
var authorRel = document.Data?.Relationships?.Author;
if (authorRel != null)
{
    // Data is strongly-typed as JsonApiResourceIdentifier
    Console.WriteLine($"Author: {authorRel.Data?.Type}/{authorRel.Data?.Id}");

    // Navigate relationship links
    if (authorRel.Links?.Related != null)
    {
        Console.WriteLine($"Fetch author at: {authorRel.Links["related"].Href}");
    }
}

// Access collection relationships (strongly-typed)
var commentsRel = document.Data?.Relationships?.Comments;
if (commentsRel?.Data != null)
{
    Console.WriteLine($"Comment count: {commentsRel.Data.Count}");
    foreach (var comment in commentsRel.Data)
    {
        Console.WriteLine($"Comment ID: {comment.Id}");
    }
}
Weakly-Typed Approach (For Dynamic Schemas)
var document = JsonApiDocument.Deserialize(json);
var resource = document.Data?.Deserialize<JsonApiResource>();

// Access relationships dynamically
if (resource?.Relationships != null &&
    resource.Relationships.TryGetValue("author", out var authorRel))
{
    // Get related resource identifier
    var authorId = authorRel.Data?.Deserialize<JsonApiResourceIdentifier>();
    Console.WriteLine($"Author: {authorId?.Type}/{authorId?.Id}");

    // Navigate relationship links
    if (authorRel.Links["related"] != null)
    {
        Console.WriteLine($"Fetch author at: {authorRel.Links["related"].Href}");
    }
}

Working with Included Resources

// Find included resources
if (document.Included != null)
{
    var authors = document.Included.Where(r => r.Type == "authors");

    foreach (var author in authors)
    {
        var name = author.Attributes?["name"]?.GetValue<string>();
        Console.WriteLine($"Included author: {name}");
    }
}

Handling Collections

// Use strongly-typed collection document with custom resource class
var collection = JsonApiCollectionDocument<ArticleResource>.Deserialize(json);

if (collection.Data != null)
{
    foreach (var article in collection.Data)
    {
        // Access strongly-typed attributes with IntelliSense
        Console.WriteLine($"Article: {article.Attributes?.Title}");
        Console.WriteLine($"Published: {article.Attributes?.PublishedAt}");

        // Access relationships
        var authorId = article.Relationships?.Author?.Data?.Id;
        Console.WriteLine($"Author ID: {authorId}");
    }
}

// Access collection-level links (pagination)
if (collection.Links["next"] != null)
{
    Console.WriteLine($"Next page: {collection.Links["next"].Href}");
}
if (collection.Links?["prev"] != null)
{
    Console.WriteLine($"Previous page: {collection.Links["prev"].Href}");
}
Weakly-Typed Approach (For Dynamic Schemas)
// Deserialize collection manually
var document = JsonApiDocument.Deserialize(json);
var articles = document.Data?.Deserialize<List<JsonApiResource>>();

if (articles != null)
{
    foreach (var article in articles)
    {
        var title = article.Attributes?["title"]?.GetValue<string>();
        Console.WriteLine($"Article: {title}");
    }
}

// Access collection-level links
if (document.Links?["next"] != null)
{
    Console.WriteLine($"Next page: {document.Links["next"].Href}");
}

HTTP Client Integration

The library provides convenient extension methods for HttpResponseMessage that integrate seamlessly with HttpClient:

using System.Net.Http;
using System.Net.Http.Headers;
using Crews.Web.JsonApiClient;

var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("application/vnd.api+json")
);

// Strongly-typed collection - ReadJsonApiCollectionDocumentAsync<T>()
var response = await client.GetAsync("https://api.example.com/articles");
var collection = await response.ReadJsonApiCollectionDocumentAsync<ArticleResource>();

if (collection?.Data != null)
{
    foreach (var article in collection.Data)
    {
        Console.WriteLine($"Article: {article.Attributes?.Title}");
    }
}

// Strongly-typed single resource - ReadJsonApiDocumentAsync<T>()
var singleResponse = await client.GetAsync("https://api.example.com/articles/123");
var document = await singleResponse.ReadJsonApiDocumentAsync<ArticleResource>();

Console.WriteLine($"Title: {document?.Data?.Attributes?.Title}");

// Weakly-typed - ReadJsonApiDocumentAsync()
var weakResponse = await client.GetAsync("https://api.example.com/unknown");
var weakDoc = await weakResponse.ReadJsonApiDocumentAsync();

if (weakDoc?.HasErrors == true)
{
    foreach (var error in weakDoc.Errors!)
    {
        Console.WriteLine($"Error: {error.Title}");
    }
}
Using Custom Headers with Extensions and Profiles
using Crews.Web.JsonApiClient.Utility;

// Build JSON:API content type header with extensions and profiles
var headerBuilder = new MediaTypeHeaderBuilder()
    .AddExtension(new Uri("https://example.com/ext/atomic"))
    .AddProfile(new Uri("https://example.com/profiles/flexible-pagination"));

var mediaType = headerBuilder.Build();

// Use with HttpClient
var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue(mediaType.MediaType.ToString())
);

// Make request and deserialize in one step
var response = await client.GetAsync("https://api.example.com/articles");
var collection = await response.ReadJsonApiCollectionDocumentAsync<ArticleResource>();

// Access strongly-typed data
if (collection?.Data != null)
{
    foreach (var article in collection.Data)
    {
        Console.WriteLine($"Article: {article.Attributes?.Title}");
    }
}
Extension Methods Available

The library provides three extension methods on HttpResponseMessage:

  1. ReadJsonApiDocumentAsync() - Deserializes to a weakly-typed JsonApiDocument

    JsonApiDocument? doc = await response.ReadJsonApiDocumentAsync();
    
  2. ReadJsonApiDocumentAsync<T>() - Deserializes to a strongly-typed JsonApiDocument<T> with a single resource

    JsonApiDocument<ArticleResource>? doc = await response.ReadJsonApiDocumentAsync<ArticleResource>();
    
  3. ReadJsonApiCollectionDocumentAsync<T>() - Deserializes to a strongly-typed JsonApiCollectionDocument<T> with a collection

    JsonApiCollectionDocument<ArticleResource>? collection =
        await response.ReadJsonApiCollectionDocumentAsync<ArticleResource>();
    

All methods support:

  • Optional JsonSerializerOptions for custom serialization behavior
  • CancellationToken for cancellation support
  • Automatic error document handling (errors deserialize naturally into Errors property)

Error Handling

if (document.HasErrors)
{
    foreach (var error in document.Errors)
    {
        Console.WriteLine($"Status: {error.Status}");
        Console.WriteLine($"Code: {error.Code}");
        Console.WriteLine($"Title: {error.Title}");
        Console.WriteLine($"Detail: {error.Detail}");

        // Error source information
        if (error.Source?.Pointer != null)
        {
            Console.WriteLine($"Source pointer: {error.Source.Pointer}");
        }

        // Error-specific links
        if (error.Links?["about"] != null)
        {
            Console.WriteLine($"More info: {error.Links["about"].Href}");
        }
    }
}
// Links can be simple strings or rich objects
var selfLink = resource.Links?["self"];

if (selfLink != null)
{
    Console.WriteLine($"URL: {selfLink.Href}");
    Console.WriteLine($"Relation: {selfLink.Rel}");
    Console.WriteLine($"Title: {selfLink.Title}");
    Console.WriteLine($"Media Type: {selfLink.Type}");

    // Language hints
    if (selfLink.HrefLang != null)
    {
        var languages = selfLink.HrefLang.GetValue<string[]>();
        Console.WriteLine($"Languages: {string.Join(", ", languages)}");
    }

    // Nested describedBy link
    if (selfLink.DescribedBy != null)
    {
        Console.WriteLine($"Described by: {selfLink.DescribedBy.Href}");
    }
}

Serialization (Round-trip)

// Create a document
var newDocument = new JsonApiDocument
{
    Data = JsonSerializer.SerializeToElement(new JsonApiResource
    {
        Type = "articles",
        Id = "123",
        Attributes = new JsonObject
        {
            ["title"] = "Hello World",
            ["body"] = "This is my first article"
        }
    })
};

// Serialize back to JSON
var json = JsonSerializer.Serialize(newDocument, new JsonSerializerOptions
{
    WriteIndented = true
});

Features

  • Strongly-typed deserialization - Define custom JsonApiResource<T> classes and get compile-time safety, IntelliSense, and refactoring support
  • HttpClient integration - Extension methods for HttpResponseMessage (ReadJsonApiDocumentAsync(), ReadJsonApiDocumentAsync<T>(), ReadJsonApiCollectionDocumentAsync<T>())
  • Simple static methods - Use JsonApiDocument<T>.Deserialize() and JsonApiCollectionDocument<T>.Deserialize() for easy JSON parsing
  • Generic subclasses for strongly-typed resources, relationships, and documents with full type safety
  • Dual typing approach - Fall back to weakly-typed base classes for dynamic schemas when needed
  • Strongly-typed models for all JSON:API specification elements
  • Flexible attribute storage using JsonObject for dynamic schemas or strongly-typed classes for known schemas
  • Dual-format link support (string URLs or rich link objects)
  • Extension support via [JsonExtensionData] for custom JSON:API extensions
  • Helper methods for safe document type checking (HasErrors, HasCollectionResource)
  • HTTP header utilities for building spec-compliant Content-Type headers with extensions and profiles
  • .NET 8.0 target with nullable reference types enabled
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Crews.Web.JsonApiClient:

Package Downloads
Crews.PlanningCenter.Api

A comprehensive library for the Planning Center API.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.0.0 430 3/7/2026
5.2.3 115 2/25/2026
5.2.2 111 2/25/2026
5.2.1 111 2/25/2026
5.2.0 114 2/25/2026
5.1.0 121 2/25/2026
5.0.0 120 2/20/2026
4.0.0 120 2/16/2026