Skip to content

Commit 62d937c

Browse files
authored
Merge pull request #3 from twcrews/dev
4.0.0
2 parents 072fec3 + c1f1aab commit 62d937c

16 files changed

Lines changed: 106 additions & 158 deletions

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [4.0.0] - 2026-02-16
9+
10+
### Changed
11+
12+
- **Breaking change:** All `Links` property types are now `Dictionary<string, JsonApiLink>`.
13+
- **Breaking change:** All property names now match their names in the JSON:API specification, with the exception of remaining pascal-cased.
14+
- All `Metadata` properties have been renamed to `Meta`.
15+
- `JsonApiError.StatusCode` has been renamed to `Status`.
16+
- `JsonApiError.ErrorCode` has been renamed to `Code`.
17+
- `JsonApiError.Details` has been renamed to `Detail`.
18+
- `JsonApiInfo.Extensions` has been renamed to `Ext`.
19+
- `JsonApiInfo.Profiles` has been renamed to `Profile`.
20+
- `JsonApiLink.HrefLanguage` has been renamed to `HrefLang`.
21+
- `JsonApiResourceIdentifier.LocalId` has been renamed to `LId`.
22+
23+
### Removed
24+
25+
- **Breaking change:** `JsonApiLinksObject` has been removed.
26+
27+
### Remarks
28+
29+
This version primarily fixes a critical deviation from the JSON:API specification in which `links` objects inside `relationships` objects were incorrectly typed as arrays, leading to deserialization exceptions. In the process of fixing this bug, the entire `JsonApiLinksObject` was removed, as it was foreign to the JSON:API specification for links.
30+
31+
Additionally, this version aims to be more idiomatic by renaming class properties to match their serialized representations.
32+
833
## [3.0.0] - 2026-02-09
934

1035
### Added
@@ -42,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4267

4368
Initial release.
4469

70+
[4.0.0]: https://github.com/twcrews/jsonapi-client/compare/3.0.0...4.0.0
4571
[3.0.0]: https://github.com/twcrews/jsonapi-client/compare/2.0.0...3.0.0
4672
[2.0.0]: https://github.com/twcrews/jsonapi-client/compare/1.0.0...2.0.0
4773
[1.0.0]: https://github.com/twcrews/jsonapi-client/releases/tag/1.0.0

Crews.Web.JsonApiClient.Tests/Converters/JsonApiLinkConverterTests.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public void ReadDeserializesStringValue()
2626
Assert.Null(result.Rel);
2727
Assert.Null(result.Title);
2828
Assert.Null(result.Type);
29-
Assert.Null(result.HrefLanguage);
30-
Assert.Null(result.Metadata);
29+
Assert.Null(result.HrefLang);
30+
Assert.Null(result.Meta);
3131
Assert.Null(result.DescribedBy);
3232
}
3333

@@ -43,8 +43,8 @@ public void ReadDeserializesObjectWithHrefOnly()
4343
Assert.Null(result.Rel);
4444
Assert.Null(result.Title);
4545
Assert.Null(result.Type);
46-
Assert.Null(result.HrefLanguage);
47-
Assert.Null(result.Metadata);
46+
Assert.Null(result.HrefLang);
47+
Assert.Null(result.Meta);
4848
Assert.Null(result.DescribedBy);
4949
}
5050

@@ -69,9 +69,9 @@ public void ReadDeserializesObjectWithAllProperties()
6969
Assert.Equal("self", result.Rel);
7070
Assert.Equal("Article Title", result.Title);
7171
Assert.Equal("text/html", result.Type);
72-
Assert.Equal("en-US", result.HrefLanguage);
73-
Assert.NotNull(result.Metadata);
74-
Assert.Equal(10, result.Metadata["count"]!.GetValue<int>());
72+
Assert.Equal("en-US", result.HrefLang);
73+
Assert.NotNull(result.Meta);
74+
Assert.Equal(10, result.Meta["count"]!.GetValue<int>());
7575
Assert.Null(result.DescribedBy);
7676
}
7777

@@ -177,7 +177,7 @@ public void WriteSerializesLinkWithHrefLangAsObject()
177177
JsonApiLink link = new()
178178
{
179179
Href = new("https://example.com/articles"),
180-
HrefLanguage = "en-US"
180+
HrefLang = "en-US"
181181
};
182182

183183
string result = JsonSerializer.Serialize(link, _options);
@@ -194,7 +194,7 @@ public void WriteSerializesLinkWithMetadataAsObject()
194194
JsonApiLink link = new()
195195
{
196196
Href = new("https://example.com/articles"),
197-
Metadata = metadata
197+
Meta = metadata
198198
};
199199

200200
string result = JsonSerializer.Serialize(link, _options);
@@ -241,8 +241,8 @@ public void WriteSerializesLinkWithAllPropertiesAsObject()
241241
Rel = "self",
242242
Title = "Article Title",
243243
Type = "text/html",
244-
HrefLanguage = "en-US",
245-
Metadata = metadata,
244+
HrefLang = "en-US",
245+
Meta = metadata,
246246
DescribedBy = describedBy
247247
};
248248

@@ -273,8 +273,8 @@ public void RoundtripSerializationPreservesSimpleLink(string href)
273273
Assert.Equal(original.Rel, deserialized.Rel);
274274
Assert.Equal(original.Title, deserialized.Title);
275275
Assert.Equal(original.Type, deserialized.Type);
276-
Assert.Equal(original.HrefLanguage, deserialized.HrefLanguage);
277-
Assert.Equal(original.Metadata, deserialized.Metadata);
276+
Assert.Equal(original.HrefLang, deserialized.HrefLang);
277+
Assert.Equal(original.Meta, deserialized.Meta);
278278
Assert.Equal(original.DescribedBy, deserialized.DescribedBy);
279279
}
280280

@@ -288,8 +288,8 @@ public void RoundtripSerializationPreservesComplexLink()
288288
Rel = "self",
289289
Title = "Article Collection",
290290
Type = "application/json",
291-
HrefLanguage = "en-US",
292-
Metadata = metadata
291+
HrefLang = "en-US",
292+
Meta = metadata
293293
};
294294

295295
string json = JsonSerializer.Serialize(original, _options);
@@ -300,9 +300,9 @@ public void RoundtripSerializationPreservesComplexLink()
300300
Assert.Equal(original.Rel, deserialized.Rel);
301301
Assert.Equal(original.Title, deserialized.Title);
302302
Assert.Equal(original.Type, deserialized.Type);
303-
Assert.Equal(original.HrefLanguage, deserialized.HrefLanguage);
304-
Assert.NotNull(deserialized.Metadata);
305-
Assert.Equal(42, deserialized.Metadata["count"]!.GetValue<int>());
306-
Assert.Equal("article", deserialized.Metadata["type"]!.GetValue<string>());
303+
Assert.Equal(original.HrefLang, deserialized.HrefLang);
304+
Assert.NotNull(deserialized.Meta);
305+
Assert.Equal(42, deserialized.Meta["count"]!.GetValue<int>());
306+
Assert.Equal("article", deserialized.Meta["type"]!.GetValue<string>());
307307
}
308308
}

Crews.Web.JsonApiClient.Tests/HttpResponseMessageExtensionsTests.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Net;
2-
using System.Net.Http;
32
using System.Text;
43
using System.Text.Json;
54

@@ -67,7 +66,7 @@ public async Task ReadJsonApiDocumentAsyncDeserializesErrorDocument()
6766
Assert.NotNull(doc);
6867
Assert.True(doc.HasErrors);
6968
JsonApiError error = doc.Errors!.First();
70-
Assert.Equal("404", error.StatusCode);
69+
Assert.Equal("404", error.Status);
7170
Assert.Equal("Not Found", error.Title);
7271
}
7372

@@ -186,8 +185,7 @@ public async Task ReadJsonApiDocumentAsyncDeserializesDocumentWithAllProperties(
186185
Assert.NotNull(doc.JsonApi);
187186
Assert.Equal("1.1", doc.JsonApi.Version);
188187
Assert.NotNull(doc.Links);
189-
Assert.NotNull(doc.Links.Self);
190-
Assert.NotNull(doc.Metadata);
191-
Assert.Equal("2024", doc.Metadata["copyright"]!.GetValue<string>());
188+
Assert.NotNull(doc.Meta);
189+
Assert.Equal("2024", doc.Meta["copyright"]!.GetValue<string>());
192190
}
193191
}

Crews.Web.JsonApiClient.Tests/JsonApiDocumentTests.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ public void DeserializesDocumentWithJsonApiProperty()
122122
Assert.NotNull(doc);
123123
Assert.NotNull(doc.JsonApi);
124124
Assert.Equal("1.1", doc.JsonApi.Version);
125-
Assert.NotNull(doc.JsonApi.Metadata);
126-
Assert.Equal("my-server", doc.JsonApi.Metadata["server"]!.GetValue<string>());
125+
Assert.NotNull(doc.JsonApi.Meta);
126+
Assert.Equal("my-server", doc.JsonApi.Meta["server"]!.GetValue<string>());
127127
}
128128

129129
[Fact(DisplayName = "Deserializes document with Links property")]
@@ -142,8 +142,7 @@ public void DeserializesDocumentWithLinksProperty()
142142

143143
Assert.NotNull(doc);
144144
Assert.NotNull(doc.Links);
145-
Assert.NotNull(doc.Links.Self);
146-
Assert.Equal("https://example.com/articles", doc.Links.Self.Href.OriginalString);
145+
Assert.Equal("https://example.com/articles", doc.Links.First().Value.Href.OriginalString);
147146
}
148147

149148
[Fact(DisplayName = "Deserializes document with Included property")]
@@ -187,9 +186,9 @@ public void DeserializesDocumentWithMetadataProperty()
187186
TestJsonApiDocument? doc = JsonSerializer.Deserialize<TestJsonApiDocument>(json, _options);
188187

189188
Assert.NotNull(doc);
190-
Assert.NotNull(doc.Metadata);
191-
Assert.Equal("Copyright 2024 Example Corp.", doc.Metadata["copyright"]!.GetValue<string>());
192-
JsonArray? authors = doc.Metadata["authors"]!.AsArray();
189+
Assert.NotNull(doc.Meta);
190+
Assert.Equal("Copyright 2024 Example Corp.", doc.Meta["copyright"]!.GetValue<string>());
191+
JsonArray? authors = doc.Meta["authors"]!.AsArray();
193192
Assert.NotNull(authors);
194193
Assert.Equal(2, authors.Count);
195194
Assert.Equal("John Doe", authors[0]!.GetValue<string>());
@@ -217,9 +216,9 @@ public void DeserializesDocumentWithErrorsProperty()
217216
Assert.NotNull(doc.Errors);
218217
JsonApiError[] errors = doc.Errors.ToArray();
219218
Assert.Single(errors);
220-
Assert.Equal("403", errors[0].StatusCode);
219+
Assert.Equal("403", errors[0].Status);
221220
Assert.Equal("Forbidden", errors[0].Title);
222-
Assert.Equal("You do not have permission to access this resource.", errors[0].Details);
221+
Assert.Equal("You do not have permission to access this resource.", errors[0].Detail);
223222
}
224223

225224
[Fact(DisplayName = "Deserializes document with Extensions property")]
@@ -307,12 +306,12 @@ public void SerializesDocumentWithAllProperties()
307306
{
308307
JsonApi = new JsonApiInfo { Version = "1.1" },
309308
Data = JsonSerializer.SerializeToElement(new JsonApiResource { Type = "articles", Id = "1" }),
310-
Links = new JsonApiLinksObject { Self = new JsonApiLink { Href = new("https://example.com/articles") } },
309+
Links = new() { { "self", new JsonApiLink { Href = new("https://example.com/articles") } } },
311310
Included =
312311
[
313312
new JsonApiResource { Type = "people", Id = "9" }
314313
],
315-
Metadata = new JsonObject { ["copyright"] = "2024" }
314+
Meta = new JsonObject { ["copyright"] = "2024" }
316315
};
317316

318317
string json = JsonSerializer.Serialize(doc, _options);
@@ -416,9 +415,9 @@ public void RoundtripSerializationPreservesErrorDocument()
416415
Assert.True(deserialized.HasErrors);
417416
Assert.NotNull(deserialized.Errors);
418417
JsonApiError error = deserialized.Errors.First();
419-
Assert.Equal("422", error.StatusCode);
418+
Assert.Equal("422", error.Status);
420419
Assert.Equal("Validation Error", error.Title);
421-
Assert.Equal("Name is required", error.Details);
420+
Assert.Equal("Name is required", error.Detail);
422421
}
423422

424423
#endregion

Crews.Web.JsonApiClient/Converters/JsonApiLinkConverter.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ internal class JsonApiLinkConverter : JsonConverter<JsonApiLink>
5353
link.Type = reader.GetString();
5454
break;
5555
case "hreflang":
56-
link.HrefLanguage = reader.GetString();
56+
link.HrefLang = reader.GetString();
5757
break;
5858
case "meta":
59-
link.Metadata = JsonSerializer.Deserialize<JsonObject>(ref reader, options);
59+
link.Meta = JsonSerializer.Deserialize<JsonObject>(ref reader, options);
6060
break;
6161
default:
6262
// Skip unknown properties
@@ -84,8 +84,8 @@ public override void Write(Utf8JsonWriter writer, JsonApiLink value, JsonSeriali
8484
value.DescribedBy is null &&
8585
string.IsNullOrEmpty(value.Title) &&
8686
string.IsNullOrEmpty(value.Type) &&
87-
string.IsNullOrEmpty(value.HrefLanguage) &&
88-
value.Metadata is null)
87+
string.IsNullOrEmpty(value.HrefLang) &&
88+
value.Meta is null)
8989
{
9090
writer.WriteStringValue(value.Href.OriginalString);
9191
return;
@@ -112,13 +112,13 @@ value.DescribedBy is null &&
112112
if (!string.IsNullOrEmpty(value.Type))
113113
writer.WriteString("type", value.Type);
114114

115-
if (!string.IsNullOrEmpty(value.HrefLanguage))
116-
writer.WriteString("hreflang", value.HrefLanguage);
115+
if (!string.IsNullOrEmpty(value.HrefLang))
116+
writer.WriteString("hreflang", value.HrefLang);
117117

118-
if (value.Metadata is not null)
118+
if (value.Meta is not null)
119119
{
120120
writer.WritePropertyName("meta");
121-
JsonSerializer.Serialize(writer, value.Metadata, options);
121+
JsonSerializer.Serialize(writer, value.Meta, options);
122122
}
123123

124124
writer.WriteEndObject();

Crews.Web.JsonApiClient/Crews.Web.JsonApiClient.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<PropertyGroup>
1111
<PackageId>Crews.Web.JsonApiClient</PackageId>
12-
<PackageVersion>3.0.0</PackageVersion>
12+
<PackageVersion>4.0.0</PackageVersion>
1313
<Authors>Tommy Crews</Authors>
1414
<Description>
1515
A library containing serialization models and methods for the JSON:API specification.

Crews.Web.JsonApiClient/HttpResponseMessageExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Net.Http;
21
using System.Net.Http.Json;
32
using System.Text.Json;
43

Crews.Web.JsonApiClient/JsonApiDocument.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class JsonApiDocument
3232
/// </summary>
3333
/// <seealso href="https://jsonapi.org/format/#document-links"/>
3434
[JsonPropertyName("links")]
35-
public JsonApiLinksObject? Links { get; set; }
35+
public Dictionary<string, JsonApiLink>? Links { get; set; }
3636

3737
/// <summary>
3838
/// Gets or sets the <c>included</c> property of the document.
@@ -45,7 +45,7 @@ public class JsonApiDocument
4545
/// </summary>
4646
/// <seealso href="https://jsonapi.org/format/#document-meta"/>
4747
[JsonPropertyName("meta")]
48-
public JsonObject? Metadata { get; set; }
48+
public JsonObject? Meta { get; set; }
4949

5050
/// <summary>
5151
/// Gets or sets members defined by any applied JSON:API extensions.

Crews.Web.JsonApiClient/JsonApiError.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ public class JsonApiError
2424
/// Gets or sets the HTTP status code associated with the error.
2525
/// </summary>
2626
[JsonPropertyName("status")]
27-
public string? StatusCode { get; set; }
27+
public string? Status { get; set; }
2828

2929
/// <summary>
3030
/// Gets or sets the application-specific error code associated with the error.
3131
/// </summary>
3232
[JsonPropertyName("code")]
33-
public string? ErrorCode { get; set; }
33+
public string? Code { get; set; }
3434

3535
/// <summary>
3636
/// Gets or sets the title of the error.
@@ -42,7 +42,7 @@ public class JsonApiError
4242
/// Gets or sets the detailed description of the error.
4343
/// </summary>
4444
[JsonPropertyName("detail")]
45-
public string? Details { get; set; }
45+
public string? Detail { get; set; }
4646

4747
/// <summary>
4848
/// Gets or sets the source of the error.
@@ -54,5 +54,5 @@ public class JsonApiError
5454
/// Gets or sets the additional metadata associated with the object.
5555
/// </summary>
5656
[JsonPropertyName("meta")]
57-
public JsonObject? Metadata { get; set; }
57+
public JsonObject? Meta { get; set; }
5858
}

Crews.Web.JsonApiClient/JsonApiInfo.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ public class JsonApiInfo
1818
/// Gets or sets the collection of extension URIs associated with the document.
1919
/// </summary>
2020
[JsonPropertyName("ext")]
21-
public IEnumerable<Uri>? Extensions { get; set; }
21+
public IEnumerable<Uri>? Ext { get; set; }
2222

2323
/// <summary>
2424
/// Gets or sets the collection of profile URIs associated with the document.
2525
/// </summary>
2626
[JsonPropertyName("profile")]
27-
public IEnumerable<Uri>? Profiles { get; set; }
27+
public IEnumerable<Uri>? Profile { get; set; }
2828

2929
/// <summary>
3030
/// Gets or sets the collection of additional metadata associated with the object.
3131
/// </summary>
3232
[JsonPropertyName("meta")]
33-
public JsonObject? Metadata { get; set; }
33+
public JsonObject? Meta { get; set; }
3434
}

0 commit comments

Comments
 (0)