# Declared personal data — say whose a row is, and the platform answers for it

> An entity that says WHOSE personal data a row is — one field marked `[DataSubject]` — and which privacy category it is held under — `[PersonalData(Category)]` — needs no hand-written privacy code. The compiler writes its `Osysharp.Privacy` holder: a person's request counts its rows, exports them, keeps them restricted or erases them, and they appear by name in every plan and in the records of processing.

<!-- id: security-declared-personal-data · area: security · stability: preview · html: https://osysharp.com/reference/security/declared-personal-data/ -->

## Summary        {#summary}
Under the GDPR a person may ask what you hold about them, take it with them, have it restricted or erased. For most of
an app's own tables the answer follows from two facts: **whose** data a row is, and **why** it is kept. Say both, and
the platform does the rest:

```osy syntax
[PersonalData(SupportNotes)]               // why: a privacy category, declared once as a function
entity Ticket {
  [DataSubject] Customer Customer;         // whose: the customer the ticket is for
  [Classification(Personal.Content)] string? RootCause;
}
```

## Signature      {#signature}
```osy syntax
DataCategory SupportNotes() => new DataCategory { Key = "app.notes", Label = "…", Purpose = "…", Basis = … };

[PersonalData(SupportNotes)]                       // erasure blanks the row's own Personal.* fields; the row stays
[PersonalData(SupportNotes, OnErasure.DeleteRow)]  // erasure deletes the row — it is wholly the person's

[DataSubject] Person Owner;                        // the app's [Principal] — found by the account a request proved
[DataSubject] Ticket Ticket;                       // another [PersonalData] row — that row's person
[DataSubject] Customer Customer;                   // a type a [FindsDataSubject] class finds (a party, by its addresses)
[DataSubject(Identifier.Email)] string Email;      // the person's own address — or Identifier.Phone, a number

[FindsDataSubject]
public class CustomersOf { public List<Customer> Find(DataSubject s) { … } }
```

## Description    {#description}
**What the platform writes.** One `IPersonalDataHolder` for the app (another for each package that declares its own),
discovered like every holder, keyed by the app's name. For each category:

| a request asks | the holder |
|---|---|
| what is held (every plan) | **counts** the rows about the person — rows already kept restricted included, read inside the category's purpose read |
| access, portability | **exports** each row's own values and every personal field it inherits, with when it was made |
| erasure | **erases** — blanks the row's `Personal.*` fields (`OnErasure.ClearPersonalFields`, the default), or deletes the row (`OnErasure.DeleteRow`) |
| restriction, or a kept category | **keeps each row restricted** until its period ends — counted from when it was made — or until lifted |
| — | a row a legal hold covers **as a case** makes the erasure wait ([Legal holds — nothing a dispute needs is erased or purged while it lasts](https://osysharp.com/reference/security/legal-holds/)) |

**Whose a row is.** A request carries only what it **proved** — an account, addresses, numbers — never what a holder
finds along the way. The `[DataSubject]` field says how to get from those to a row: the app's `[Principal]` (by the
account), the person's own address or number held on the row (`Identifier.Email`, `Identifier.Phone`, matched exactly),
another `[PersonalData]` row (an answer through its ticket, a ticket through its customer), or a type a
`[FindsDataSubject]` class finds. `Osysharp.Privacy.Conversations` ships `PartiesOfASubject`, so a reference to a
`Party` — or the app's own `Customer : Party` — is followed through the addresses on its contact points, parties merged
into it included. A finder is a CLASS, not a function: a public function is callable by a request, and "who holds this
address" must not be a URL.

**Which fields.** What is personal stays said on the field (`[Classification(Personal.X)]`,
[personal data (Classification(Personal.X))](https://osysharp.com/reference/security/personal-data/)). Erasure blanks every personal field the row holds, its own and the ones it inherits from a
base type alike.

**Order.** A request erases the category that says how others find the person — their contact points
(`DataCategory.Identifies`) — after every other one, so a declared holder still finds the tickets of a customer whose
addresses go in the same request. A declared category whose rows other declared rows are found through, and which an
erasure deletes, is marked so too: in the example below a member's calls go before the preference that says whose
they are. Within each step, the declared holder's erasure runs before every holder you wrote by hand. So a hand-written
holder that writes something in place of a blanked field keeps it. For example, a ticket's title, which was the
customer's subject, can become "Erased conversation" instead of staying blank.

**Refused at compile** (`PERSONAL_DATA_DECLARATION`), each naming the fix: a `[PersonalData]` entity with no — or two —
`[DataSubject]` fields; a reference no request can follow; a category that is not a parameterless `DataCategory`
function, or whose `Key` is not a literal (it names the category's purpose reads and access-trail rows); a required
personal field that is not text, when erasure keeps the row; a row that stays on erasure pointing at one that is
deleted (the delete would be refused); `[PersonalData]` in an app that does not use
`Osysharp.Privacy`; and — for now — an entity whose erasure keeps the row in a KEPT category (a legal basis with a
period), because a kept row is purged by deletion when its period ends.

**When to write a holder instead.** Anything the declaration cannot say: a reason an erasure must wait ("an order is on
its way"), a category kept with another (`KeptWith`), a kept complaint read only for a claim, files written about a row.
A hand-written holder and a declared one live side by side in one app.

## Examples       {#examples}
```osy title="a club's own rows: a member's preference, found by their address" test app=security-declared-personal-data
app Club {
  use Osysharp.Identity;
  use Osysharp.WorkQueues;
  use Osysharp.Mail;
  use Osysharp.Privacy;
  use Osysharp.Workflow;
  use Osysharp.Observability;
  use Osysharp.Storage;
}

using Osysharp.Identity;
using Osysharp.Privacy;

[Role] enum ClubRole { Authenticator, Admin }
policy IsAdmin => RoleGrant.Any(g => g.Member == user && g.Role == ClubRole.Admin);
policy ManagesPrivacy => IsAdmin;
policy PlacesLegalHolds => IsAdmin;
policy WorksInbox => IsAdmin;
policy LeadsInbox => IsAdmin;
policy AdministersInbox => IsAdmin;
policy ManagesOrganizations => IsAdmin;
policy CollaboratesOnRecords => IsAdmin;

/// Why the club keeps a member's newsletter preference.
public DataCategory Preferences() {
  return new DataCategory { Key = "club.preferences", Label = "Your newsletter preference",
                            Purpose = "Sending you the newsletter", SubjectKind = "members",
                            Basis = RetentionBasis.Consent, Portable = true };
}

/// What we wrote down when a member called us — wholly about them, so an erasure deletes it.
public DataCategory CallNotes() {
  return new DataCategory { Key = "club.calls", Label = "Notes from your calls to us",
                            Purpose = "Helping you", SubjectKind = "members", Basis = RetentionBasis.Contract };
}

[PersonalData(Preferences, OnErasure.DeleteRow)]
entity Preference {
  [DataSubject(Identifier.Email), Classification(Personal.Contact), MaxLength(320)] string Email;
  bool Newsletter;
  [CreatedAt] DateTime CreatedAt;
  security { allow read, create, update, delete when IsAdmin; }
}

[PersonalData(CallNotes, OnErasure.DeleteRow)]
entity Call {
  [DataSubject] Preference Member;
  DateTime At;
  [Classification(Personal.Content), MaxLength(2000)] string? Notes;
  [CreatedAt] DateTime CreatedAt;
  security { allow read, create, update, delete when IsAdmin; }
}

// The club's own two levels, read by whoever may read the row (the admins); the kits' levels need no line.
app = new() { Classifications = [ new Classification(Personal.Contact) { Open = true },
                                  new Classification(Personal.Content) { Open = true } ] };
```

## See also       {#see-also}
- [The Privacy kit — erasure that keeps only what the company must or may](https://osysharp.com/reference/security/privacy-kit/) — categories, requests, plans and the purge
- [personal data (Classification(Personal.X))](https://osysharp.com/reference/security/personal-data/) — saying a field is personal data, and who reads it
- [restricted rows and purpose reads (Restriction, [PurposeRead])](https://osysharp.com/reference/security/restriction/) — what "kept restricted" means
- [Legal holds — nothing a dispute needs is erased or purged while it lasts](https://osysharp.com/reference/security/legal-holds/) — a hold keeps a person's data, or a case, while a dispute needs it
