Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.0.0] - 2026-02-20

### Changed

- **Breaking change:** All model types have been changed from `class`es to `record`s.
- **Breaking change:** All property accessors on model types have changed from `{ get; set; }` to `{ get; init; }`.

### Removed

- **Breaking change:** `JsonApiLinksObject` has been removed.

### Remarks

This version refactors the entire model into immutable `record` types. While opinionated, I'm convinced this change is in the best interest of creating robust and resilient code.

If you're not willing to part with mutable `class`es, this version is otherwise identical to version 4.0.0.

## [4.0.0] - 2026-02-16

### Changed
Expand Down Expand Up @@ -67,6 +84,7 @@ Additionally, this version aims to be more idiomatic by renaming class propertie

Initial release.

[5.0.0]: https://github.com/twcrews/jsonapi-client/compare/4.0.0...5.0.0
[4.0.0]: https://github.com/twcrews/jsonapi-client/compare/3.0.0...4.0.0
[3.0.0]: https://github.com/twcrews/jsonapi-client/compare/2.0.0...3.0.0
[2.0.0]: https://github.com/twcrews/jsonapi-client/compare/1.0.0...2.0.0
Expand Down
12 changes: 6 additions & 6 deletions Crews.Web.JsonApiClient.Tests/JsonApiDocumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public JsonApiDocumentTests()
}

// Concrete implementation for testing abstract JsonApiDocument
private class TestJsonApiDocument : JsonApiDocument { }
private record TestJsonApiDocument : JsonApiDocument { }

#region HasCollectionResource Tests

Expand Down Expand Up @@ -475,15 +475,15 @@ public void DeserializeCollectionGenericStaticMethodReturnsValidDocumentForValid
Assert.True(doc.HasCollectionResource);
}

public class MyModel
public record MyModel
{
public string? Name { get; set; }
public int Age { get; set; }
public string? Name { get; init; }
public int Age { get; init; }
}

public class MyModelResource : JsonApiResource<MyModel> { }
public record MyModelResource : JsonApiResource<MyModel> { }

public class MyModelDocument : JsonApiDocument<MyModelResource> { }
public record MyModelDocument : JsonApiDocument<MyModelResource> { }

#endregion
}
40 changes: 30 additions & 10 deletions Crews.Web.JsonApiClient/Converters/JsonApiLinkConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,32 @@ internal class JsonApiLinkConverter : JsonConverter<JsonApiLink>
// Case 2: Link is an object with properties
if (reader.TokenType == JsonTokenType.StartObject)
{
JsonApiLink link = new() { Href = new(string.Empty, UriKind.Relative) };
Uri? href = null;
string? rel = null;
JsonApiLink? describedBy = null;
string? title = null;
string? type = null;
string? hrefLang = null;
JsonObject? meta = null;

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
return link;
{
if (href is null)
throw new JsonException("Href is required for link objects.");

return new JsonApiLink
{
Href = href,
Rel = rel,
DescribedBy = describedBy,
Title = title,
Type = type,
HrefLang = hrefLang,
Meta = meta
};
}

if (reader.TokenType == JsonTokenType.PropertyName)
{
Expand All @@ -37,26 +57,26 @@ internal class JsonApiLinkConverter : JsonConverter<JsonApiLink>
switch (propertyName)
{
case "href":
string href = reader.GetString() ?? throw new JsonException("Href cannot be null.");
link.Href = new(href);
string hrefString = reader.GetString() ?? throw new JsonException("Href cannot be null.");
href = new(hrefString);
break;
case "rel":
link.Rel = reader.GetString();
rel = reader.GetString();
break;
case "describedby":
link.DescribedBy = JsonSerializer.Deserialize<JsonApiLink>(ref reader, options);
describedBy = JsonSerializer.Deserialize<JsonApiLink>(ref reader, options);
break;
case "title":
link.Title = reader.GetString();
title = reader.GetString();
break;
case "type":
link.Type = reader.GetString();
type = reader.GetString();
break;
case "hreflang":
link.HrefLang = reader.GetString();
hrefLang = reader.GetString();
break;
case "meta":
link.Meta = JsonSerializer.Deserialize<JsonObject>(ref reader, options);
meta = JsonSerializer.Deserialize<JsonObject>(ref reader, options);
break;
default:
// Skip unknown properties
Expand Down
2 changes: 1 addition & 1 deletion Crews.Web.JsonApiClient/Crews.Web.JsonApiClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<PropertyGroup>
<PackageId>Crews.Web.JsonApiClient</PackageId>
<PackageVersion>4.0.0</PackageVersion>
<PackageVersion>5.0.0</PackageVersion>
<Authors>Tommy Crews</Authors>
<Description>
A library containing serialization models and methods for the JSON:API specification.
Expand Down
24 changes: 12 additions & 12 deletions Crews.Web.JsonApiClient/JsonApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,51 @@ namespace Crews.Web.JsonApiClient;
/// <summary>
/// Represents a base class for JSON:API top-level objects as defined in section 7.1 of the JSON:API specification.
/// </summary>
public class JsonApiDocument
public record JsonApiDocument
{
/// <summary>
/// Gets or sets the <c>jsonapi</c> property of the document.
/// </summary>
[JsonPropertyName("jsonapi")]
public JsonApiInfo? JsonApi { get; set; }
public JsonApiInfo? JsonApi { get; init; }

/// <summary>
/// Gets or sets the primary data payload associated with the document.
/// </summary>
[JsonPropertyName("data")]
public JsonElement? Data { get; set; }
public JsonElement? Data { get; init; }

/// <summary>
/// Gets or sets the collection of errors associated with the document.
/// </summary>
[JsonPropertyName("errors")]
public IEnumerable<JsonApiError>? Errors { get; set; }
public IEnumerable<JsonApiError>? Errors { get; init; }

/// <summary>
/// Gets or sets the <c>links</c> property of the document.
/// </summary>
/// <seealso href="https://jsonapi.org/format/#document-links"/>
[JsonPropertyName("links")]
public Dictionary<string, JsonApiLink>? Links { get; set; }
public Dictionary<string, JsonApiLink>? Links { get; init; }

/// <summary>
/// Gets or sets the <c>included</c> property of the document.
/// </summary>
[JsonPropertyName("included")]
public IEnumerable<JsonApiResource>? Included { get; set; }
public IEnumerable<JsonApiResource>? Included { get; init; }

/// <summary>
/// Gets or sets the <c>meta</c> property of the document.
/// </summary>
/// <seealso href="https://jsonapi.org/format/#document-meta"/>
[JsonPropertyName("meta")]
public JsonObject? Meta { get; set; }
public JsonObject? Meta { get; init; }

/// <summary>
/// Gets or sets members defined by any applied JSON:API extensions.
/// </summary>
[JsonExtensionData]
public Dictionary<string, JsonElement>? Extensions { get; set; }
public Dictionary<string, JsonElement>? Extensions { get; init; }

/// <summary>
/// Gets a value indicating whether the <see cref="Data"/> property contains a resource collection object.
Expand Down Expand Up @@ -87,13 +87,13 @@ public class JsonApiDocument
/// JSON:API specification.
/// </summary>
/// <typeparam name="T">The underlying resource type.</typeparam>
public class JsonApiDocument<T> : JsonApiDocument where T : JsonApiResource
public record JsonApiDocument<T> : JsonApiDocument where T : JsonApiResource
{
/// <summary>
/// Gets or sets the primary data payload associated with the document.
/// </summary>
[JsonPropertyName("data")]
public new T? Data { get; set; }
public new T? Data { get; init; }

/// <summary>
/// Gets a value indicating whether the <see cref="Data"/> property contains a single resource object.
Expand All @@ -120,13 +120,13 @@ public class JsonApiDocument<T> : JsonApiDocument where T : JsonApiResource
/// JSON:API specification.
/// </summary>
/// <typeparam name="T">The underlying resource type.</typeparam>
public class JsonApiCollectionDocument<T> : JsonApiDocument where T : JsonApiResource
public record JsonApiCollectionDocument<T> : JsonApiDocument where T : JsonApiResource
{
/// <summary>
/// Gets or sets the primary data payload associated with the document.
/// </summary>
[JsonPropertyName("data")]
public new IEnumerable<T>? Data { get; set; }
public new IEnumerable<T>? Data { get; init; }

/// <summary>
/// Gets a value indicating whether the <see cref="Data"/> property contains a resource collection object.
Expand Down
18 changes: 9 additions & 9 deletions Crews.Web.JsonApiClient/JsonApiError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,53 @@ namespace Crews.Web.JsonApiClient;
/// <summary>
/// Represents an error in a document as defined in section 11.2 of the JSON:API specification.
/// </summary>
public class JsonApiError
public record JsonApiError
{
/// <summary>
/// Gets or sets the unique identifier for this particular occurrence of the problem.
/// </summary>
[JsonPropertyName("id")]
public string? Id { get; set; }
public string? Id { get; init; }

/// <summary>
/// Gets or sets the links that provide additional information about the error.
/// </summary>
[JsonPropertyName("links")]
public JsonApiErrorLinksObject? Links { get; set; }
public JsonApiErrorLinksObject? Links { get; init; }

/// <summary>
/// Gets or sets the HTTP status code associated with the error.
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; set; }
public string? Status { get; init; }

/// <summary>
/// Gets or sets the application-specific error code associated with the error.
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; set; }
public string? Code { get; init; }

/// <summary>
/// Gets or sets the title of the error.
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
public string? Title { get; init; }

/// <summary>
/// Gets or sets the detailed description of the error.
/// </summary>
[JsonPropertyName("detail")]
public string? Detail { get; set; }
public string? Detail { get; init; }

/// <summary>
/// Gets or sets the source of the error.
/// </summary>
[JsonPropertyName("source")]
public JsonApiErrorSource? Source { get; set; }
public JsonApiErrorSource? Source { get; init; }

/// <summary>
/// Gets or sets the additional metadata associated with the object.
/// </summary>
[JsonPropertyName("meta")]
public JsonObject? Meta { get; set; }
public JsonObject? Meta { get; init; }
}
8 changes: 4 additions & 4 deletions Crews.Web.JsonApiClient/JsonApiErrorLinksObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@ namespace Crews.Web.JsonApiClient;
/// <summary>
/// Represents a set of links that provide additional information about an error in a JSON:API document.
/// </summary>
public class JsonApiErrorLinksObject
public record JsonApiErrorLinksObject
{
/// <summary>
/// Gets or sets a link that provides additional information about the error.
/// </summary>
[JsonPropertyName("about")]
[JsonConverter(typeof(JsonApiLinkConverter))]
public JsonApiLink? About { get; set; }
public JsonApiLink? About { get; init; }

/// <summary>
/// Gets or sets the link that specifies the type of the error.
/// </summary>
[JsonPropertyName("type")]
[JsonConverter(typeof(JsonApiLinkConverter))]
public JsonApiLink? Type { get; set; }
public JsonApiLink? Type { get; init; }

/// <summary>
/// Gets or sets a collection of additional JSON properties that are not mapped to known members.
/// </summary>
[JsonExtensionData]
public Dictionary<string, JsonElement>? Extensions { get; set; }
public Dictionary<string, JsonElement>? Extensions { get; init; }
}
8 changes: 4 additions & 4 deletions Crews.Web.JsonApiClient/JsonApiErrorSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ namespace Crews.Web.JsonApiClient;
/// Represents the source of an error in a request, such as a specific field, parameter, or header that caused the
/// error.
/// </summary>
public class JsonApiErrorSource
public record JsonApiErrorSource
{
/// <summary>
/// Gets or sets the JSON Pointer that identifies the location within a JSON document that caused the error.
/// </summary>
[JsonPropertyName("pointer")]
public string? Pointer { get; set; }
public string? Pointer { get; init; }

/// <summary>
/// Gets or sets the URI query parameter value that caused the error.
/// </summary>
[JsonPropertyName("parameter")]
public string? Parameter { get; set; }
public string? Parameter { get; init; }

/// <summary>
/// Gets or sets the name of a request header that caused the error.
/// </summary>
[JsonPropertyName("header")]
public string? Header { get; set; }
public string? Header { get; init; }
}
10 changes: 5 additions & 5 deletions Crews.Web.JsonApiClient/JsonApiInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,29 @@ namespace Crews.Web.JsonApiClient;
/// <summary>
/// Represents information about the JSON:API implementation as defined in section 7.7 of the JSON:API specification.
/// </summary>
public class JsonApiInfo
public record JsonApiInfo
{
/// <summary>
/// Gets or sets the JSON:API version for the document.
/// </summary>
[JsonPropertyName("version")]
public string? Version { get; set; }
public string? Version { get; init; }

/// <summary>
/// Gets or sets the collection of extension URIs associated with the document.
/// </summary>
[JsonPropertyName("ext")]
public IEnumerable<Uri>? Ext { get; set; }
public IEnumerable<Uri>? Ext { get; init; }

/// <summary>
/// Gets or sets the collection of profile URIs associated with the document.
/// </summary>
[JsonPropertyName("profile")]
public IEnumerable<Uri>? Profile { get; set; }
public IEnumerable<Uri>? Profile { get; init; }

/// <summary>
/// Gets or sets the collection of additional metadata associated with the object.
/// </summary>
[JsonPropertyName("meta")]
public JsonObject? Meta { get; set; }
public JsonObject? Meta { get; init; }
}
Loading