A comprehensive TypeScript library for validating and working with Swedish personal identification numbers (personnummer), coordination numbers (samordningsnummer), and organisation numbers (organisationsnummer).
getAge/isAdult/isChildon Swedish ID classes now anchor to Europe/Stockholm civil time when called without a clock argument. v1 anchored to UTC and returned the wrong age during the 1–2 hour window between Stockholm and UTC midnight every birthday.- Leap-day birthdays (born 29 February) now attain age on 28 February in non-leap years per Lag (1930:173) §1. v1 reported them as one year younger on 28 February.
- Removed from
@deathbycode/civitas-id-core:LocalDate.now(),LocalDate.prototype.age(),IdValidator,ValidationResult,IdFormat. Replacement:computeAge(birth, today, resolver)with the country package'sAnniversaryResolver. The Sweden ID classes apply this internally; consumers usingLocalDate.now()orlocalDate.age(ref)directly must migrate. - Public method signatures of
PersonalId/CoordinationId/OrganisationIdare unchanged.
- Overview
- Features
- Installation
- Usage Examples
- Error Handling
- Format Options
- Type Guards & Discriminated Unions
- Test Utilities (Fakers)
- Architecture
- Core Utilities
- Requirements
- Building the Project
- API Design
- Swedish Organisation Number Format
- Organisation Forms
- License
This library provides comprehensive functionality for working with Swedish official identification numbers according to official rules and formats from Skatteverket and Bolagsverket.
- Personal Numbers (personnummer): Validation and formatting of Swedish personal ID numbers
- Coordination Numbers (samordningsnummer): Support for coordination numbers assigned to individuals without Swedish personal numbers
- Support for different formats (10-digit, 12-digit, with or without separators)
- Handling of personal numbers from different time periods (19th, 20th, and 21st centuries)
- Birth date extraction and age calculation
- Gender determination (male/female)
- Organisation Numbers (organisationsnummer): Validation and formatting of Swedish organisation numbers
- Organisation Form Detection: Automatic detection of organisation type (e.g., Aktiebolag, Ekonomiska foreningar, etc.)
- Support for all 33 official Swedish organisation forms
- Distinction between legal persons (juridisk person) and physical persons (enskild firma)
- Hybrid Format Support: Accepts both official 10-digit format and legacy 12-digit format for compatibility
- Input: Accepts
556012-3456(10-digit) or165560123456(12-digit legacy) - Output: Always returns official 10-digit format
- Input: Accepts
- Checksum validation using the Luhn algorithm
- Type-safe API with discriminated unions and type guards
- Stockholm civil-time anchoring for legally correct age computation under Swedish statute
- Lag (1930:173) §1 leap-day rule for 29 February birthdays
- Zero runtime dependencies
- Test utilities (fakers) for generating valid test data
npm install @deathbycode/civitas-id-sweden
# or
pnpm add @deathbycode/civitas-id-swedenimport { PersonalId } from "@deathbycode/civitas-id-sweden";
// Parse and validate a personal number (returns undefined on failure)
const personalId = PersonalId.parse("202407132394");
if (personalId) {
const birthDate = personalId.getBirthDate(); // LocalDate { year: 2024, month: 7, day: 13 }
const longFormat = personalId.longFormat(); // "202407132394"
}
// Or use parseOrThrow() when you expect valid input
const id = PersonalId.parseOrThrow("202407132394");
const birthDate = id.getBirthDate();
// Format in different ways
id.longFormat(); // "202407132394"
id.shortFormat(); // "2407132394"
id.shortFormatWithSeparator(); // "240713-2394"
// Check gender
id.isMale(); // true or false
id.isFemale(); // true or false
// Check age (anchored to Europe/Stockholm civil time when called with no arg)
id.getAge(); // current age in years
id.isAdult(); // true if 18+
id.isChild(); // true if under 18
// Check if valid
PersonalId.isValid("202407132394"); // trueimport { CoordinationId } from "@deathbycode/civitas-id-sweden";
// Parse a coordination number (returns undefined on failure)
const coordId = CoordinationId.parse("198206822390");
// Or use parseOrThrow() when you expect valid input
const id = CoordinationId.parseOrThrow("198206822390");
// Coordination numbers have birth day + 60
const birthDate = id.getBirthDate(); // 1982-06-22 (not 1982-06-82)import { OrganisationId, OrganisationNumberType } from "@deathbycode/civitas-id-sweden";
// Parse an organisation number (accepts both 10 and 12-digit formats)
const orgId = OrganisationId.parse("556012-3456");
// Or use parseOrThrow() when you expect valid input
const id = OrganisationId.parseOrThrow("556012-3456");
// Get the Skatteverket form code (BEST-EFFORT heuristic — see note below)
const form = id.getOrganisationForm();
console.log(form.code); // 49
console.log(form.description); // "Övriga aktiebolag"
// Get the AUTHORITATIVE legal-class bucket (Bolagsverket first-digit allocation)
const cls = id.getLegalClass();
console.log(cls.name); // "LIMITED_COMPANY"
console.log(cls.firstDigit); // 5
console.log(cls.description); // "Aktiebolag (inkl. banker, försäkringsbolag, europabolag)"
// Check if legal or physical person
id.isLegalPerson(); // true for companies
id.isPhysicalPerson(); // true for sole proprietors
// Format (always outputs 10-digit official format)
id.longFormatWithSeparator(); // "556012-3456"
// Validate with type checking
OrganisationId.isValid("556012-3456", OrganisationNumberType.LEGAL_PERSON);
getOrganisationForm()vsgetLegalClass()— the Skatteverket form code surfaced bygetOrganisationForm()is a best-effort heuristic that derives a form code from the first two digits of the orgnummer, with hand-maintained carve-outs (e.g.556and559→ Aktiebolag). It works most of the time because Bolagsverket's prefix allocation and Skatteverket's form-code numbering overlap, but it is not a contract. PrefergetLegalClass()when you need the structural legal-form bucket (limited company vs association vs foundation, etc.) — that mapping is derived directly from Bolagsverket's authoritative first-digit prefix allocation. Query Bolagsverket directly when you need the authoritative Skatteverket form code for a specific entity.
When you don't know the specific type of Swedish ID, use the unified parsing methods:
import {
SwedishOfficialId,
PersonalId,
CoordinationId,
OrganisationId,
InvalidIdNumberError,
isPersonalId,
isCoordinationId,
isOrganisationId,
} from "@deathbycode/civitas-id-sweden";
// Parse any Swedish ID type (returns undefined on failure)
const result = SwedishOfficialId.parseAny("202407132394");
if (result) {
// Use type guards to narrow the type
if (isPersonalId(result)) {
console.log("Personal ID:", result.getBirthDate());
} else if (isCoordinationId(result)) {
console.log("Coordination ID:", result.getBirthDate());
} else if (isOrganisationId(result)) {
console.log("Organisation ID:", result.getOrganisationForm());
}
}
// Or use parseAnyOrThrow() when you expect valid input
try {
const id = SwedishOfficialId.parseAnyOrThrow("202407132394");
// Use discriminated union with switch
switch (id.type) {
case "PERSONAL":
console.log(id.getBirthDate());
break;
case "COORDINATION":
console.log(id.getBirthDate());
break;
case "ORGANISATION":
console.log(id.getOrganisationForm());
break;
}
} catch (e) {
if (e instanceof InvalidIdNumberError) {
console.error("Invalid Swedish ID:", e.message);
} else {
throw e;
}
}
// Validate any Swedish ID type
SwedishOfficialId.isValid("202407132394"); // trueParsing Priority: The unified parser attempts to parse in this order:
- Personal number (personnummer)
- Coordination number (samordningsnummer)
- Organisation number (organisationsnummer)
All failures from the throwing APIs (parseOrThrow, parseAnyOrThrow, format) throw InvalidIdNumberError, which extends the standard Error class and supports error cause chaining via ErrorOptions. Non-throwing parse helpers (parse, parseAny) return undefined on failure:
import { PersonalId, InvalidIdNumberError } from "@deathbycode/civitas-id-sweden";
// Catch and inspect parsing errors
try {
const id = PersonalId.parseOrThrow("invalid-input");
} catch (e) {
if (e instanceof InvalidIdNumberError) {
console.error(e.message); // "Invalid personal ID: invalid-input"
}
}
// Error cause chaining — wrap lower-level errors
try {
processId(input);
} catch (e) {
throw new InvalidIdNumberError("Failed to process ID", { cause: e });
}Personal and coordination IDs support six output formats via the PnrFormat union. The separator character is age-sensitive: - for persons under 100, + for centenarians.
| PnrFormat Value | Pattern | Example (born 1990-05-15) | Example (born 1890-05-15) |
|---|---|---|---|
LONG_FORMAT |
YYYYMMDDXXXX |
199005151239 |
189005151239 |
LONG_FORMAT_WITH_STANDARD_SEPARATOR |
YYYYMMDD-XXXX |
19900515-1239 |
18900515-1239 |
LONG_FORMAT_WITH_SEPARATOR |
YYYYMMDD-XXXX or YYYYMMDD+XXXX |
19900515-1239 |
18900515+1239 |
SHORT_FORMAT |
YYMMDDXXXX |
9005151239 |
9005151239 |
SHORT_FORMAT_WITH_STANDARD_SEPARATOR |
YYMMDD-XXXX |
900515-1239 |
900515-1239 |
SHORT_FORMAT_WITH_SEPARATOR |
YYMMDD-XXXX or YYMMDD+XXXX |
900515-1239 |
900515+1239 |
import { PersonalId, PnrFormat } from "@deathbycode/civitas-id-sweden";
const id = PersonalId.parseOrThrow("199005151239");
// Convenience methods (use standard separator)
id.longFormat(); // "199005151239"
id.shortFormat(); // "9005151239"
id.longFormatWithSeparator(); // "19900515-1239"
id.shortFormatWithSeparator(); // "900515-1239"
// Explicit format selection
id.formatted(PnrFormat.LONG_FORMAT_WITH_SEPARATOR); // "19900515-1239" (or +)
id.formatted(PnrFormat.SHORT_FORMAT_WITH_SEPARATOR); // "900515-1239" (or +)Organisation IDs also accept
PnrFormatin theirformatted()method, but long and short variants produce identical output since organisation numbers are always 10 digits (the internal16century prefix is always stripped).
SwedishOfficialId is a union type with a type discriminant field:
type SwedishOfficialId = PersonalId | CoordinationId | OrganisationId;Each variant carries a type field: "PERSONAL", "COORDINATION", or "ORGANISATION".
Four type guard functions are exported for narrowing:
import {
SwedishOfficialId,
isPersonalId,
isCoordinationId,
isOrganisationId,
isPersonOfficialId,
} from "@deathbycode/civitas-id-sweden";
const id = SwedishOfficialId.parseAnyOrThrow(input);
// Narrow to a specific type
if (isPersonalId(id)) {
id.getBirthDate(); // PersonalId methods available
}
if (isCoordinationId(id)) {
id.getBirthDate(); // CoordinationId methods available
}
if (isOrganisationId(id)) {
id.getOrganisationForm(); // OrganisationId methods available
}
// Narrow to person types (PersonalId | CoordinationId)
if (isPersonOfficialId(id)) {
id.getBirthDate(); // shared person methods available
id.getAge();
id.isMale();
}switch (id.type) {
case "PERSONAL":
console.log("Personal ID:", id.getBirthDate());
break;
case "COORDINATION":
console.log("Coordination ID:", id.getBirthDate());
break;
case "ORGANISATION":
console.log("Organisation ID:", id.getOrganisationForm());
break;
}The library includes test utilities for generating valid test data, available via a separate subpath export to keep them out of production bundles:
import { PersonalIdFaker } from "@deathbycode/civitas-id-sweden/testing";import { PersonalIdFaker } from "@deathbycode/civitas-id-sweden/testing";
import { LocalDate } from "@deathbycode/civitas-id-core";
// Generate random valid personal ID
const randomId = PersonalIdFaker.create();
// Generate with specific birth date
const specificId = PersonalIdFaker.create(LocalDate.of(1990, 5, 15));
// Generate with specific date components
const id = PersonalIdFaker.createFor(1990, 5, 15);
// Generate gender-specific IDs
const male = PersonalIdFaker.createMale();
const female = PersonalIdFaker.createFemale();
// Generate centenarian (100+ years old)
const centenarian = PersonalIdFaker.createCentenarian();import { CoordinationIdFaker } from "@deathbycode/civitas-id-sweden/testing";
// Generate random coordination ID
const randomId = CoordinationIdFaker.create();
// Gender-specific and centenarian methods also available
const male = CoordinationIdFaker.createMale();
const female = CoordinationIdFaker.createFemale();
const centenarian = CoordinationIdFaker.createCentenarian();import { SwedishOrganisationIdFaker } from "@deathbycode/civitas-id-sweden/testing";
// Generate random organisation ID (legal person)
const legalPerson = SwedishOrganisationIdFaker.create();
// Generate specific types
const legal = SwedishOrganisationIdFaker.createLegalPerson();
const physical = SwedishOrganisationIdFaker.createPhysicalPerson();import { SwedishOfficialIdFaker } from "@deathbycode/civitas-id-sweden/testing";
// Generate random Swedish ID (PersonalId, CoordinationId, or OrganisationId)
const randomId = SwedishOfficialIdFaker.create();
// Generate multiple IDs at once
const ids = SwedishOfficialIdFaker.createMany(10);Note: All fakers generate cryptographically secure random IDs using crypto.getRandomValues() and ensure proper Luhn checksum validation.
The library is architected for multi-country support with a clear separation of concerns:
civitas-id/
├── @deathbycode/civitas-id-core — Generic interfaces and utilities
│ ├── OfficialId, PersonOfficialId, OrganisationOfficialId interfaces
│ ├── Generic Luhn algorithm implementation
│ └── LocalDate value object
└── @deathbycode/civitas-id-sweden — Swedish implementations
├── PersonalId, CoordinationId, OrganisationId
├── Swedish-specific validation and formatting
└── @deathbycode/civitas-id-sweden/testing — Test fakers for generating Swedish IDs
Design Principles:
- Core package: Contains only country-agnostic interfaces and truly generic utilities
- Country packages: Implement core interfaces with country-specific validation rules
- Test utilities: Available via
@deathbycode/civitas-id-sweden/testingsubpath to keep production bundles clean - Discriminated unions: Type-safe ID hierarchy using TypeScript discriminated unions with
typefield - Idiomatic TypeScript: Default generic parameters, exhaustiveness guards, discriminated union results, const object singletons
Extensibility: The architecture is ready for additional countries (Norway, Finland, Denmark, etc.) following the same pattern. Each country module is self-contained and independent.
A minimal, immutable date value object from @deathbycode/civitas-id-core. No external date library dependencies.
import { LocalDate, computeAge } from "@deathbycode/civitas-id-core";
// Factories
const date = LocalDate.of(1990, 5, 15); // from components
const parsed = LocalDate.parse("1990-05-15"); // from ISO string
// Read-only properties
date.year; // 1990
date.month; // 5
date.day; // 15
// Methods
date.isValid(); // true if the date exists in the calendar
date.equals(other); // structural equality
date.toString(); // "1990-05-15"LocalDate deliberately does not expose now() or age() — clock access and age computation are jurisdiction-bound. Sweden's getAge/isAdult/isChild methods automatically anchor to Europe/Stockholm when called with no argument; the todayInSweden() clock helper and swedishAnniversaryResolver (implementing Lag (1930:173) §1) are used internally and are not part of the public surface.
Clock injection: Methods like getAge() on Swedish ID objects accept an optional clock function () => LocalDate so tests can control the current date. The default anchor is todayInSweden() (legally correct for Swedish civil age):
const id = PersonalId.parseOrThrow("199005151239");
id.getAge(() => LocalDate.of(2026, 1, 1)); // 35 — deterministic for tests
id.getAge(); // anchored to Europe/Stockholm civil dateA const object implementing the standard Luhn (mod-10) checksum algorithm, from @deathbycode/civitas-id-core:
import { LuhnAlgorithm } from "@deathbycode/civitas-id-core";
// Calculate the check digit for a digit string
LuhnAlgorithm.calculateCheckDigit("7992739871"); // 3
// Verify a complete number (last digit is the check digit)
LuhnAlgorithm.isChecksumValid("79927398713"); // trueBoth methods accept an optional maxDigits parameter to limit validation to the last N digits of the input. The Swedish variant uses maxDigits=10 internally so that 12-digit personal numbers (YYYYMMDDXXXX) are validated on only the 10-digit suffix (YYMMDDXXXX).
- Node.js 18+
- TypeScript 5.7+ (for consumers using TypeScript)
# Install dependencies and build all packages
pnpm install
# Run all tests
pnpm run testSee BUILDING.md for the full development setup guide.
The library provides a safe API with two parsing approaches:
| Method | Return Type | Use Case |
|---|---|---|
parse(string) |
T | undefined |
Uncertain input (user-provided, external APIs) |
parseOrThrow(string) |
T |
Expected valid input (database, known data) |
isValid(string) |
boolean |
Validation only |
All ID types implement the OfficialId interface and share a common set of format methods:
| Method | PersonalId (1990-05-15) | OrganisationId (556012-3456) |
|---|---|---|
longFormat() |
"199005151239" |
"5560123456" |
shortFormat() |
"9005151239" |
"5560123456" |
longFormatWithSeparator() |
"19900515-1239" |
"556012-3456" |
shortFormatWithSeparator() |
"900515-1239" |
"556012-3456" |
formatted(PnrFormat) |
(see Format Options) | (long = short for org IDs) |
For organisation IDs, long and short variants produce identical output because organisation numbers are always 10 digits.
The base SwedishOfficialId namespace provides unified parsing when the ID type is unknown:
| Method | Return Type | Use Case |
|---|---|---|
parseAny(string) |
SwedishOfficialId | undefined |
Parse any type (uncertain input) |
parseAnyOrThrow(string) |
SwedishOfficialId |
Parse any type (expected valid) |
Swedish organisation numbers are officially ALWAYS 10 digits.
Format: NNNNNN-NNNN where:
- Positions 1-2: Legal entity group code (organisation form)
- Position 3: Always >= 2 (to distinguish from personal numbers)
- Position 10: Check digit (Luhn algorithm)
The "16" prefix (e.g., 16NNNNNNNNNN) is not an official standard and stems from legacy IT systems using "16" as a placeholder century.
Civitas-ID implementation:
- Input: Accepts both 10-digit (
556012-3456) and 12-digit (165560123456) for legacy compatibility - Output: Always returns the official 10-digit format
// Both inputs work
const org1 = OrganisationId.parseOrThrow("556012-3456"); // 10-digit
const org2 = OrganisationId.parseOrThrow("165560123456"); // 12-digit legacy
// Output is always 10-digit
org1.longFormat(); // "5560123456"
org2.longFormat(); // "5560123456"
org1.longFormatWithSeparator(); // "556012-3456"
org2.longFormatWithSeparator(); // "556012-3456"The library supports all 33 official Swedish organisation forms. The organisation form is automatically extracted from the first two digits of the organisation number.
| Code | Organisation Form (Swedish) | Description |
|---|---|---|
| 0 | Ingen organisationsform | Physical person (not a legal entity) |
| 21 | Enkla bolag | Simple companies |
| 22 | Partrederier | Shipping partnerships |
| 31 | Handelsbolag, kommanditbolag | Trading partnerships, limited partnerships |
| 32 | Gruvbolag | Mining companies |
| 41 | Bankaktiebolag | Banking companies |
| 42 | Forsakringsaktiebolag | Insurance companies |
| 43 | Europabolag | European companies (SE) |
| 49 | Ovriga aktiebolag | Other limited companies (most common) |
| 51 | Ekonomiska foreningar | Economic associations |
| 53 | Bostadsrattsforeningar | Tenant-ownership associations |
| 54 | Kooperativ Hyresrattsforening | Cooperative rental associations |
| 55 | Europakooperativ, EGTS och Eric-konsortier | European cooperatives, EGTC, Eric consortia |
| 61 | Ideella foreningar | Non-profit associations |
| 62 | Samfalligheter | Joint property management associations |
| 63 | Registrerat trossamfund | Registered religious communities |
| 71 | Familjestiftelser | Family foundations |
| 72 | Ovriga stiftelser och fonder | Other foundations and funds |
| 81 | Statliga enheter | State entities |
| 82 | Kommuner | Municipalities |
| 83 | Kommunalforbund | Municipal federations |
| 84 | Regioner | Regions |
| 85 | Allmanna forsakringskassor | General insurance funds |
| 87 | Offentliga korporationer och anstalter | Public corporations and agencies |
| 88 | Hypoteksforeningar | Mortgage associations |
| 89 | Regionala statliga myndigheter | Regional state authorities |
| 91 | Oskiftade dodsbon | Undivided estates |
| 92 | Omsesidiga forsakringsbolag | Mutual insurance companies |
| 93 | Sparbanker | Savings banks |
| 94 | Understodsforeningar och Forsakringsforeningar | Benefit societies and insurance associations |
| 95 | Arbetsloshetskassor | Unemployment insurance funds |
| 96 | Utlandska juridiska personer | Foreign legal entities |
| 98 | Ovriga svenska juridiska personer | Other Swedish legal entities (special legislation) |
| 99 | Juridisk form ej utredd | Legal form not determined |
Total: 34 forms (1 physical person code + 33 legal entity forms)
Source: Swedish Companies Registration Office (Bolagsverket)
This project is licensed under the MIT License - see the LICENSE file for details.
- Skatteverket (Swedish Tax Agency): https://www.skatteverket.se
- Bolagsverket (Companies Registration Office): https://www.bolagsverket.se
- SCB (Statistics Sweden): https://www.scb.se