# V2033 opaque_not_schema

URL: /docs/errors/V2033
Markdown: /docs/errors/V2033.md

> A Schema bound (Json.decode, Sql.query rows, Llm.generate, Cli.parse, Tool.of arguments, a property parameter) reaches an opaque type outside its declaring module, directly or through a field (design §17.130).

An error from the `type` phase. `sakura explain V2033` prints this explanation offline.

A `Schema` bound (`Json.decode`, `Sql.query` rows, `Llm.generate`, `Cli.parse`, `Tool.of`
arguments, a `property` parameter) reaches an opaque type outside its declaring module, directly
or through a field (design §17.130). Private construction comes in two kinds: an invariant that
is a predicate over the representation is a refinement type and decodes through its predicate
(`Uuid`, `Path`, `Decimal`); an invariant about how the value was produced (`Query<R>` from a
`sql"…"` literal, `Html` by escaping, `UrlPart` by encoding, `File` by the runtime) is opaque,
and data cannot build one. Decode the representation and call the module's constructor in the
open.

Wrong:

```sakura
type Report = { title: String, body: Html }
let r: Report = Json.decode(text)?
```

Fixed:

```sakura
type ReportIn = { title: String, body: String }
let r: ReportIn = Json.decode(text)?
let body = Html.text(r.body)
```

## Wrong program [#wrong-program]

[`compiler/tests/ui/V2033_decode_opaque_html.sakura`](https://github.com/icantcodefyi/sakuralang/blob/master/compiler/tests/ui/V2033_decode_opaque_html.sakura), which the compiler's ui suite runs through `sakura check`:

```sakura
// The WS9.10 re-audit's second exploit: markup forged by decoding JSON into `Html`, then
// rendered through `html"…"`. `Html` is opaque outside `std/html`, so it is not `Schema` here.
import std/html.{Html}
import std/json.{DecodeError}

pub fn main() -> Unit ! {Console} {
  let forged: Result<Html, DecodeError> = Json.decode("\{\"Html\":\"<script>evil()</script>\"\}")
  match forged {
    Ok(h) => Console.print(Html.to_string(html"<p>{h}</p>")),
    Err(_) => Console.print("rejected"),
  }
}
```

`sakura check --json` reports this record (pretty-printed; `$DIR` is the fixture's directory):

```json
{
  "schema_version": "1.0",
  "kind": "diagnostic",
  "id": "d:0a7b90073701a01b",
  "code": "V2033",
  "severity": "error",
  "phase": "type",
  "message": "`Html` is opaque outside `std/html`, so it is not `Schema`: a value comes from its module's constructors, not from data (design §17.130)",
  "snapshot": "sha256:$SNAPSHOT",
  "symbol_id": "main::V2033_decode_opaque_html::main",
  "spans": [
    {
      "uri": "file://$DIR/V2033_decode_opaque_html.sakura",
      "content_hash": "sha256:a7a9afd859632742e97a2d058c182883ad7d476ce6c8569202ea2e954ccf8ec2",
      "start_byte": 385,
      "end_byte": 396,
      "role": "primary"
    }
  ],
  "related_ids": [],
  "expected": {
    "render": "Schema"
  },
  "actual": {
    "render": "Html"
  },
  "obligation": null,
  "counterexample": null,
  "explanation_uri": "sakura://explain/V2033",
  "fixes": []
}
```

More fixtures for this code: [`compiler/tests/ui/V2033_decode_opaque_query.sakura`](https://github.com/icantcodefyi/sakuralang/blob/master/compiler/tests/ui/V2033_decode_opaque_query.sakura), [`compiler/tests/ui/V2033_decode_opaque_urlpart.sakura`](https://github.com/icantcodefyi/sakuralang/blob/master/compiler/tests/ui/V2033_decode_opaque_urlpart.sakura), [`compiler/tests/ui/V2033_property_opaque.sakura`](https://github.com/icantcodefyi/sakuralang/blob/master/compiler/tests/ui/V2033_property_opaque.sakura).

## Fixed program [#fixed-program]

[`compiler/tests/ui/V2033_decode_opaque_html.fixed.sakura`](https://github.com/icantcodefyi/sakuralang/blob/master/compiler/tests/ui/V2033_decode_opaque_html.fixed.sakura), which `sakura check` accepts with no diagnostics:

```sakura
// Decode the representation (a `String`), then build the `Html` through its escaping
// constructor.
import std/html.{Html}
import std/json.{DecodeError}

pub fn main() -> Unit ! {Console} {
  let decoded: Result<String, DecodeError> = Json.decode("\"<script>evil()</script>\"")
  match decoded {
    Ok(s) => Console.print(Html.to_string(html"<p>{Html.text(s)}</p>")),
    Err(_) => Console.print("rejected"),
  }
}
```
