diff --git a/Semantics.Strings.Identifiers/IsIsbnAttribute.cs b/Semantics.Strings.Identifiers/IsIsbnAttribute.cs
index 4a57653e..02acbf5c 100644
--- a/Semantics.Strings.Identifiers/IsIsbnAttribute.cs
+++ b/Semantics.Strings.Identifiers/IsIsbnAttribute.cs
@@ -8,8 +8,8 @@ namespace ktsu.Semantics.Strings.Identifiers;
///
/// Validates that the string is an ISBN-10 (weighted mod-11, final digit may be X) or an ISBN-13
-/// (mod-10), evaluated on the separator-stripped value. Registration-group and publisher ranges are
-/// not validated.
+/// (mod-10, with the 978 or 979 Bookland prefix), evaluated on the separator-stripped
+/// value. Registration-group and publisher ranges are not validated.
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class IsIsbnAttribute : NativeSemanticStringValidationAttribute
@@ -63,6 +63,12 @@ private static bool IsValidIsbn10(string s)
private static bool IsValidIsbn13(string s)
{
+ // An ISBN-13 is an EAN-13 in the Bookland range; any other prefix is a non-book barcode.
+ if (!s.StartsWith("978", StringComparison.Ordinal) && !s.StartsWith("979", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
int sum = 0;
for (int i = 0; i < 13; i++)
{
diff --git a/Semantics.Strings.Identifiers/Isbn.cs b/Semantics.Strings.Identifiers/Isbn.cs
index 74c9d040..81c00b01 100644
--- a/Semantics.Strings.Identifiers/Isbn.cs
+++ b/Semantics.Strings.Identifiers/Isbn.cs
@@ -7,8 +7,9 @@ namespace ktsu.Semantics.Strings.Identifiers;
///
/// An International Standard Book Number in ISBN-10 or ISBN-13 form, e.g. 0306406152 or
/// 9780306406157. Hyphens and spaces are stripped and the value is uppercased (for the ISBN-10
-/// X check digit) on creation. The check digit is validated; registration-group and publisher
-/// ranges are not.
+/// X check digit) on creation. The check digit is validated, and an ISBN-13 must start with
+/// the 978 or 979 Bookland prefix; registration-group and publisher ranges are not
+/// validated.
///
[IsIsbn]
public sealed record Isbn : SemanticString
diff --git a/Semantics.Strings.Identifiers/README.md b/Semantics.Strings.Identifiers/README.md
index 4e70ea77..f2f3b45f 100644
--- a/Semantics.Strings.Identifiers/README.md
+++ b/Semantics.Strings.Identifiers/README.md
@@ -87,7 +87,7 @@ Each type is a `sealed record` deriving from `SemanticString`. None declare t
| `Uuid` | Trim, strip wrapping `{}` / `()`, lowercase | Canonical 8-4-4-4-12 hex (RFC 4122 layout) | Any variant/version accepted, including the nil UUID. Not version-checked. |
| `Ulid` | Trim, uppercase | 26 Crockford base32 chars, first char `0`-`7` | Timestamp is not otherwise decoded. |
| `Iban` | Strip spaces, uppercase | Length 15-34, country/check prefix, ISO 7064 mod-97-10 checksum | Country-specific BBAN structure is not enforced. |
-| `Isbn` | Strip `-` and spaces, uppercase | ISBN-10 (weighted mod-11, `X` allowed) or ISBN-13 (mod-10) | Registration-group/publisher ranges are not validated. |
+| `Isbn` | Strip `-` and spaces, uppercase | ISBN-10 (weighted mod-11, `X` allowed) or ISBN-13 (`978`/`979` prefix, mod-10) | Registration-group/publisher ranges are not validated. |
| `CreditCardNumber` | Strip spaces and hyphens | 13-19 digits, Luhn (mod-10) checksum | Luhn only. No issuer/network detection, no PCI guarantee. The value is sensitive, do not log it. |
| `JwtToken` | None (stored verbatim, case-sensitive) | Three `.`-separated segments, non-empty header and payload that base64url-decode to UTF-8 text delimited as `{ ... }` | Structural only: the header and payload bodies are not parsed, so malformed JSON between the braces is accepted. Signature is not decoded or verified. `alg`, claims, and expiry are not inspected. |
diff --git a/Semantics.Test/Identifiers/IsbnTests.cs b/Semantics.Test/Identifiers/IsbnTests.cs
index 8d404ec2..cc044222 100644
--- a/Semantics.Test/Identifiers/IsbnTests.cs
+++ b/Semantics.Test/Identifiers/IsbnTests.cs
@@ -40,6 +40,30 @@ public void Create_BadIsbn13CheckDigit_Throws()
Assert.ThrowsExactly(() => Isbn.Create("978-0-306-40615-8"));
}
+ [TestMethod]
+ public void Create_ValidIsbn13With979Prefix_Succeeds()
+ {
+ Isbn isbn = Isbn.Create("979-10-90636-07-1");
+ Assert.AreEqual("9791090636071", isbn.WeakString);
+ }
+
+ [TestMethod]
+ [DataRow("4006381333931")]
+ [DataRow("5901234123457")]
+ [DataRow("9770317847001")]
+ public void Create_Ean13OutsideBooklandRange_Throws(string ean13)
+ {
+ Assert.ThrowsExactly(() => Isbn.Create(ean13));
+ }
+
+ [TestMethod]
+ public void TryCreate_Ean13OutsideBooklandRange_ReturnsFalse()
+ {
+ bool created = Isbn.TryCreate("4006381333931", out Isbn? result);
+ Assert.IsFalse(created);
+ Assert.IsNull(result);
+ }
+
[TestMethod]
public void Create_WrongLength_Throws()
{