diff --git a/CHANGELOG.md b/CHANGELOG.md index 155743a..d86542d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/Crews.Web.JsonApiClient.Tests/JsonApiDocumentTests.cs b/Crews.Web.JsonApiClient.Tests/JsonApiDocumentTests.cs index 2cb501f..7aa4ec2 100644 --- a/Crews.Web.JsonApiClient.Tests/JsonApiDocumentTests.cs +++ b/Crews.Web.JsonApiClient.Tests/JsonApiDocumentTests.cs @@ -13,7 +13,7 @@ public JsonApiDocumentTests() } // Concrete implementation for testing abstract JsonApiDocument - private class TestJsonApiDocument : JsonApiDocument { } + private record TestJsonApiDocument : JsonApiDocument { } #region HasCollectionResource Tests @@ -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 { } + public record MyModelResource : JsonApiResource { } - public class MyModelDocument : JsonApiDocument { } + public record MyModelDocument : JsonApiDocument { } #endregion } diff --git a/Crews.Web.JsonApiClient/Converters/JsonApiLinkConverter.cs b/Crews.Web.JsonApiClient/Converters/JsonApiLinkConverter.cs index 2ab9077..cbb4ff4 100644 --- a/Crews.Web.JsonApiClient/Converters/JsonApiLinkConverter.cs +++ b/Crews.Web.JsonApiClient/Converters/JsonApiLinkConverter.cs @@ -22,12 +22,32 @@ internal class JsonApiLinkConverter : JsonConverter // 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) { @@ -37,26 +57,26 @@ internal class JsonApiLinkConverter : JsonConverter 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(ref reader, options); + describedBy = JsonSerializer.Deserialize(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(ref reader, options); + meta = JsonSerializer.Deserialize(ref reader, options); break; default: // Skip unknown properties diff --git a/Crews.Web.JsonApiClient/Crews.Web.JsonApiClient.csproj b/Crews.Web.JsonApiClient/Crews.Web.JsonApiClient.csproj index fd7cc96..2ad8471 100644 --- a/Crews.Web.JsonApiClient/Crews.Web.JsonApiClient.csproj +++ b/Crews.Web.JsonApiClient/Crews.Web.JsonApiClient.csproj @@ -9,7 +9,7 @@ Crews.Web.JsonApiClient - 4.0.0 + 5.0.0 Tommy Crews A library containing serialization models and methods for the JSON:API specification. diff --git a/Crews.Web.JsonApiClient/JsonApiDocument.cs b/Crews.Web.JsonApiClient/JsonApiDocument.cs index 09d7e90..0615360 100644 --- a/Crews.Web.JsonApiClient/JsonApiDocument.cs +++ b/Crews.Web.JsonApiClient/JsonApiDocument.cs @@ -7,51 +7,51 @@ namespace Crews.Web.JsonApiClient; /// /// Represents a base class for JSON:API top-level objects as defined in section 7.1 of the JSON:API specification. /// -public class JsonApiDocument +public record JsonApiDocument { /// /// Gets or sets the jsonapi property of the document. /// [JsonPropertyName("jsonapi")] - public JsonApiInfo? JsonApi { get; set; } + public JsonApiInfo? JsonApi { get; init; } /// /// Gets or sets the primary data payload associated with the document. /// [JsonPropertyName("data")] - public JsonElement? Data { get; set; } + public JsonElement? Data { get; init; } /// /// Gets or sets the collection of errors associated with the document. /// [JsonPropertyName("errors")] - public IEnumerable? Errors { get; set; } + public IEnumerable? Errors { get; init; } /// /// Gets or sets the links property of the document. /// /// [JsonPropertyName("links")] - public Dictionary? Links { get; set; } + public Dictionary? Links { get; init; } /// /// Gets or sets the included property of the document. /// [JsonPropertyName("included")] - public IEnumerable? Included { get; set; } + public IEnumerable? Included { get; init; } /// /// Gets or sets the meta property of the document. /// /// [JsonPropertyName("meta")] - public JsonObject? Meta { get; set; } + public JsonObject? Meta { get; init; } /// /// Gets or sets members defined by any applied JSON:API extensions. /// [JsonExtensionData] - public Dictionary? Extensions { get; set; } + public Dictionary? Extensions { get; init; } /// /// Gets a value indicating whether the property contains a resource collection object. @@ -87,13 +87,13 @@ public class JsonApiDocument /// JSON:API specification. /// /// The underlying resource type. -public class JsonApiDocument : JsonApiDocument where T : JsonApiResource +public record JsonApiDocument : JsonApiDocument where T : JsonApiResource { /// /// Gets or sets the primary data payload associated with the document. /// [JsonPropertyName("data")] - public new T? Data { get; set; } + public new T? Data { get; init; } /// /// Gets a value indicating whether the property contains a single resource object. @@ -120,13 +120,13 @@ public class JsonApiDocument : JsonApiDocument where T : JsonApiResource /// JSON:API specification. /// /// The underlying resource type. -public class JsonApiCollectionDocument : JsonApiDocument where T : JsonApiResource +public record JsonApiCollectionDocument : JsonApiDocument where T : JsonApiResource { /// /// Gets or sets the primary data payload associated with the document. /// [JsonPropertyName("data")] - public new IEnumerable? Data { get; set; } + public new IEnumerable? Data { get; init; } /// /// Gets a value indicating whether the property contains a resource collection object. diff --git a/Crews.Web.JsonApiClient/JsonApiError.cs b/Crews.Web.JsonApiClient/JsonApiError.cs index df98666..8210c20 100644 --- a/Crews.Web.JsonApiClient/JsonApiError.cs +++ b/Crews.Web.JsonApiClient/JsonApiError.cs @@ -6,53 +6,53 @@ namespace Crews.Web.JsonApiClient; /// /// Represents an error in a document as defined in section 11.2 of the JSON:API specification. /// -public class JsonApiError +public record JsonApiError { /// /// Gets or sets the unique identifier for this particular occurrence of the problem. /// [JsonPropertyName("id")] - public string? Id { get; set; } + public string? Id { get; init; } /// /// Gets or sets the links that provide additional information about the error. /// [JsonPropertyName("links")] - public JsonApiErrorLinksObject? Links { get; set; } + public JsonApiErrorLinksObject? Links { get; init; } /// /// Gets or sets the HTTP status code associated with the error. /// [JsonPropertyName("status")] - public string? Status { get; set; } + public string? Status { get; init; } /// /// Gets or sets the application-specific error code associated with the error. /// [JsonPropertyName("code")] - public string? Code { get; set; } + public string? Code { get; init; } /// /// Gets or sets the title of the error. /// [JsonPropertyName("title")] - public string? Title { get; set; } + public string? Title { get; init; } /// /// Gets or sets the detailed description of the error. /// [JsonPropertyName("detail")] - public string? Detail { get; set; } + public string? Detail { get; init; } /// /// Gets or sets the source of the error. /// [JsonPropertyName("source")] - public JsonApiErrorSource? Source { get; set; } + public JsonApiErrorSource? Source { get; init; } /// /// Gets or sets the additional metadata associated with the object. /// [JsonPropertyName("meta")] - public JsonObject? Meta { get; set; } + public JsonObject? Meta { get; init; } } diff --git a/Crews.Web.JsonApiClient/JsonApiErrorLinksObject.cs b/Crews.Web.JsonApiClient/JsonApiErrorLinksObject.cs index 45ddbe4..26c0112 100644 --- a/Crews.Web.JsonApiClient/JsonApiErrorLinksObject.cs +++ b/Crews.Web.JsonApiClient/JsonApiErrorLinksObject.cs @@ -7,25 +7,25 @@ namespace Crews.Web.JsonApiClient; /// /// Represents a set of links that provide additional information about an error in a JSON:API document. /// -public class JsonApiErrorLinksObject +public record JsonApiErrorLinksObject { /// /// Gets or sets a link that provides additional information about the error. /// [JsonPropertyName("about")] [JsonConverter(typeof(JsonApiLinkConverter))] - public JsonApiLink? About { get; set; } + public JsonApiLink? About { get; init; } /// /// Gets or sets the link that specifies the type of the error. /// [JsonPropertyName("type")] [JsonConverter(typeof(JsonApiLinkConverter))] - public JsonApiLink? Type { get; set; } + public JsonApiLink? Type { get; init; } /// /// Gets or sets a collection of additional JSON properties that are not mapped to known members. /// [JsonExtensionData] - public Dictionary? Extensions { get; set; } + public Dictionary? Extensions { get; init; } } diff --git a/Crews.Web.JsonApiClient/JsonApiErrorSource.cs b/Crews.Web.JsonApiClient/JsonApiErrorSource.cs index f649f54..7ebf811 100644 --- a/Crews.Web.JsonApiClient/JsonApiErrorSource.cs +++ b/Crews.Web.JsonApiClient/JsonApiErrorSource.cs @@ -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. /// -public class JsonApiErrorSource +public record JsonApiErrorSource { /// /// Gets or sets the JSON Pointer that identifies the location within a JSON document that caused the error. /// [JsonPropertyName("pointer")] - public string? Pointer { get; set; } + public string? Pointer { get; init; } /// /// Gets or sets the URI query parameter value that caused the error. /// [JsonPropertyName("parameter")] - public string? Parameter { get; set; } + public string? Parameter { get; init; } /// /// Gets or sets the name of a request header that caused the error. /// [JsonPropertyName("header")] - public string? Header { get; set; } + public string? Header { get; init; } } diff --git a/Crews.Web.JsonApiClient/JsonApiInfo.cs b/Crews.Web.JsonApiClient/JsonApiInfo.cs index ecd899e..23fb96a 100644 --- a/Crews.Web.JsonApiClient/JsonApiInfo.cs +++ b/Crews.Web.JsonApiClient/JsonApiInfo.cs @@ -6,29 +6,29 @@ namespace Crews.Web.JsonApiClient; /// /// Represents information about the JSON:API implementation as defined in section 7.7 of the JSON:API specification. /// -public class JsonApiInfo +public record JsonApiInfo { /// /// Gets or sets the JSON:API version for the document. /// [JsonPropertyName("version")] - public string? Version { get; set; } + public string? Version { get; init; } /// /// Gets or sets the collection of extension URIs associated with the document. /// [JsonPropertyName("ext")] - public IEnumerable? Ext { get; set; } + public IEnumerable? Ext { get; init; } /// /// Gets or sets the collection of profile URIs associated with the document. /// [JsonPropertyName("profile")] - public IEnumerable? Profile { get; set; } + public IEnumerable? Profile { get; init; } /// /// Gets or sets the collection of additional metadata associated with the object. /// [JsonPropertyName("meta")] - public JsonObject? Meta { get; set; } + public JsonObject? Meta { get; init; } } diff --git a/Crews.Web.JsonApiClient/JsonApiLink.cs b/Crews.Web.JsonApiClient/JsonApiLink.cs index 04b9207..110cab0 100644 --- a/Crews.Web.JsonApiClient/JsonApiLink.cs +++ b/Crews.Web.JsonApiClient/JsonApiLink.cs @@ -8,50 +8,50 @@ namespace Crews.Web.JsonApiClient; /// Represents a hypermedia link with associated metadata, as defined in section 7.6.1 of the JSON:API specification. /// [JsonConverter(typeof(JsonApiLinkConverter))] -public class JsonApiLink +public record JsonApiLink { /// /// Gets or sets the URL of the link. /// [JsonPropertyName("href")] - public required Uri Href { get; set; } + public required Uri Href { get; init; } /// /// Gets or sets the relation type for the link. /// [JsonPropertyName("rel")] - public string? Rel { get; set; } + public string? Rel { get; init; } /// /// Gets or sets a link to a resource that provides additional descriptive information about the current object. /// [JsonPropertyName("describedby")] [JsonConverter(typeof(JsonApiLinkConverter))] - public JsonApiLink? DescribedBy { get; set; } + public JsonApiLink? DescribedBy { get; init; } /// /// Gets or sets the title associated with the object. /// [JsonPropertyName("title")] - public string? Title { get; set; } + public string? Title { get; init; } /// /// Gets or sets the type of the object represented by this instance. /// [JsonPropertyName("type")] - public string? Type { get; set; } + public string? Type { get; init; } /// /// Gets or sets the language of the linked resource, as defined by the hreflang attribute in HTML or XML sitemaps. /// [JsonPropertyName("hreflang")] - public string? HrefLang { get; set; } + public string? HrefLang { get; init; } /// /// Gets or sets metadata about the link. /// [JsonPropertyName("meta")] - public JsonObject? Meta { get; set; } + public JsonObject? Meta { get; init; } /// /// Implicitly converts a string URL to a JsonApiLink instance. diff --git a/Crews.Web.JsonApiClient/JsonApiRelationship.cs b/Crews.Web.JsonApiClient/JsonApiRelationship.cs index ca1c26f..e6463c6 100644 --- a/Crews.Web.JsonApiClient/JsonApiRelationship.cs +++ b/Crews.Web.JsonApiClient/JsonApiRelationship.cs @@ -7,31 +7,31 @@ namespace Crews.Web.JsonApiClient; /// /// Represents a relationship object as defined in section 7.2.2.2 of the JSON:API specification. /// -public class JsonApiRelationship +public record JsonApiRelationship { /// /// Gets or sets the links property of the relationship object. /// [JsonPropertyName("links")] - public Dictionary? Links { get; set; } + public Dictionary? Links { get; init; } /// /// Gets or sets the data payload associated with the response or request. /// [JsonPropertyName("data")] - public JsonElement? Data { get; set; } + public JsonElement? Data { get; init; } /// /// Gets or sets additional metadata associated with the object. /// [JsonPropertyName("meta")] - public JsonObject? Meta { get; set; } + public JsonObject? Meta { get; init; } /// /// Gets or sets members defined by any applied JSON:API extensions. /// [JsonExtensionData] - public Dictionary? Extensions { get; set; } + public Dictionary? Extensions { get; init; } } /// @@ -39,12 +39,12 @@ public class JsonApiRelationship /// JSON:API specification. /// /// The type of the resource identifier object in the property. -public class JsonApiRelationship : JsonApiRelationship where T : JsonApiResourceIdentifier +public record JsonApiRelationship : JsonApiRelationship where T : JsonApiResourceIdentifier { /// /// Gets or sets the data payload associated with the response or request. /// - public new T? Data { get; set; } + public new T? Data { get; init; } } /// @@ -54,10 +54,10 @@ public class JsonApiRelationship : JsonApiRelationship where T : JsonApiResou /// /// The type of the resource identifier collection object in the property. /// -public class JsonApiCollectionRelationship : JsonApiRelationship where T : IEnumerable +public record JsonApiCollectionRelationship : JsonApiRelationship where T : IEnumerable { /// /// Gets or sets the data payload associated with the response or request. /// - public new T? Data { get; set; } + public new T? Data { get; init; } } \ No newline at end of file diff --git a/Crews.Web.JsonApiClient/JsonApiResource.cs b/Crews.Web.JsonApiClient/JsonApiResource.cs index 149ecd6..ae0e7b0 100644 --- a/Crews.Web.JsonApiClient/JsonApiResource.cs +++ b/Crews.Web.JsonApiClient/JsonApiResource.cs @@ -6,31 +6,31 @@ namespace Crews.Web.JsonApiClient; /// /// Represents a resource object as defined in section 7.2 of the JSON:API specification. /// -public class JsonApiResource : JsonApiResourceIdentifier +public record JsonApiResource : JsonApiResourceIdentifier { /// /// Gets or sets the collection of custom attributes associated with this object. /// [JsonPropertyName("attributes")] - public JsonObject? Attributes { get; set; } + public JsonObject? Attributes { get; init; } /// /// Gets or sets the collection of relationships associated with this object. /// [JsonPropertyName("relationships")] - public Dictionary? Relationships { get; set; } + public Dictionary? Relationships { get; init; } /// /// Gets or sets the links property of the resource object. /// [JsonPropertyName("links")] - public Dictionary? Links { get; set; } + public Dictionary? Links { get; init; } /// /// Gets or sets the custom metadata associated with this object. /// [JsonPropertyName("meta")] - public JsonObject? Meta { get; set; } + public JsonObject? Meta { get; init; } } /// @@ -38,12 +38,12 @@ public class JsonApiResource : JsonApiResourceIdentifier /// specification. /// /// The type of the property. -public class JsonApiResource : JsonApiResource +public record JsonApiResource : JsonApiResource { /// /// Gets or sets the collection of custom attributes associated with this object. /// - public new T? Attributes { get; set; } + public new T? Attributes { get; init; } } /// @@ -51,11 +51,11 @@ public class JsonApiResource : JsonApiResource /// /// The type used to represent the attributes of the resource object. /// The type used to represent the relationships associated with the resource object. -public class JsonApiResource : JsonApiResource +public record JsonApiResource : JsonApiResource { /// /// Gets or sets the collection of relationships associated with this object. /// [JsonPropertyName("relationships")] - public new TRelationships? Relationships { get; set; } + public new TRelationships? Relationships { get; init; } } \ No newline at end of file diff --git a/Crews.Web.JsonApiClient/JsonApiResourceIdentifier.cs b/Crews.Web.JsonApiClient/JsonApiResourceIdentifier.cs index 80516eb..f3c2727 100644 --- a/Crews.Web.JsonApiClient/JsonApiResourceIdentifier.cs +++ b/Crews.Web.JsonApiClient/JsonApiResourceIdentifier.cs @@ -6,23 +6,23 @@ namespace Crews.Web.JsonApiClient; /// Represents a reference to a resource by its type and identifier, as defined in section 7.3 of the JSON:API /// specification. /// -public class JsonApiResourceIdentifier +public record JsonApiResourceIdentifier { /// /// Gets or sets the unique identifier for the object. /// [JsonPropertyName("id")] - public string? Id { get; set; } + public string? Id { get; init; } /// /// Gets or sets the local identifier associated with the object. /// [JsonPropertyName("lid")] - public string? LId { get; set; } + public string? LId { get; init; } /// /// Gets or sets the type identifier for the object represented by this instance. /// [JsonPropertyName("type")] - public required string Type { get; set; } + public required string Type { get; init; } } diff --git a/README.md b/README.md index db96792..6ad908a 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ dotnet add package Crews.Web.JsonApiClient // Step 1: Define your base model public class Article { - public string? Title { get; set; } - public string? Body { get; set; } - public DateTime? PublishedAt { get; set; } + 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 @@ -109,22 +109,22 @@ public class UserResource : JsonApiResource { } public class User { [JsonPropertyName("name")] - public string? Name { get; set; } + public string? Name { get; init; } [JsonPropertyName("email")] - public string? Email { get; set; } + public string? Email { get; init; } [JsonPropertyName("createdAt")] - public DateTime? CreatedAt { get; set; } + public DateTime? CreatedAt { get; init; } } public class UserRelationships { [JsonPropertyName("posts")] - public JsonApiCollectionRelationship>? Posts { get; set; } + public JsonApiCollectionRelationship>? Posts { get; init; } [JsonPropertyName("profile")] - public JsonApiRelationship? Profile { get; set; } + public JsonApiRelationship? Profile { get; init; } } // Use the types