{"id":34676006,"url":"https://github.com/klimick/decode","last_synced_at":"2025-12-24T20:35:00.627Z","repository":{"id":49565764,"uuid":"368009866","full_name":"klimick/decode","owner":"klimick","description":"WIP: Decoding untrusted data in typesafe way","archived":false,"fork":false,"pushed_at":"2022-06-16T12:34:32.000Z","size":667,"stargazers_count":8,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-08-02T05:08:19.537Z","etag":null,"topics":["functional","functional-programming","php","psalm","psalm-plugin"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/klimick.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-05-17T00:01:23.000Z","updated_at":"2023-10-25T17:25:55.000Z","dependencies_parsed_at":"2022-09-22T14:25:53.167Z","dependency_job_id":null,"html_url":"https://github.com/klimick/decode","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/klimick/decode","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/klimick%2Fdecode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/klimick%2Fdecode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/klimick%2Fdecode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/klimick%2Fdecode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/klimick","download_url":"https://codeload.github.com/klimick/decode/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/klimick%2Fdecode/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28008430,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-24T02:00:07.193Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["functional","functional-programming","php","psalm","psalm-plugin"],"created_at":"2025-12-24T20:34:59.788Z","updated_at":"2025-12-24T20:35:00.621Z","avatar_url":"https://github.com/klimick.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Decode\n\n![psalm level](https://shepherd.dev/github/klimick/decode/level.svg)\n![psalm type coverage](https://shepherd.dev/github/klimick/decode/coverage.svg)\n[![phpunit coverage](https://coveralls.io/repos/github/klimick/decode/badge.svg)](https://coveralls.io/github/klimick/decode)\n\nThis library allow you to take untrusted data and check that it can be represented as type `T`.\n\n- [Usage example](#usage-example)\n- [Built in atomics](#builtin-type-atomics)\n- [Generic types](#generic-types)\n- [Higher order helpers](#higher-order-helpers)\n- [Constraints](#constraints)\n\n## Usage example\n\n```php\n\u003c?php\n\nuse Klimick\\Decode\\Decoder as t;\n\n// Describes runtime type for array{name: string, age: int, meta: list\u003cstring\u003e}\n$libraryDefinition = t\\shape(\n    id: t\\int(),\n    name: t\\string(),\n    meta: t\\listOf(t\\string()),\n);\n\n// Untrusted data\n$json = '{\n    \"id\": 42,\n    \"name\": \"Decode\",\n    \"meta\": [\n        \"runtime type system\",\n        \"psalm integration\",\n        \"with whsv26/functional\"\n    ]\n}';\n\n// If decode will fail, CastException is thrown.\n// $person is array{name: string, age: int, meta: list\u003cstring\u003e}\n$person = t\\tryCast(\n    value: $json,\n    to: t\\fromJson($libraryDefinition),\n);\n\n// Either data type from whsv26/functional\n// Left side contains decoding errors\n// Right side holds decoded valid\n// $person is Either\u003cInvalid, Valid\u003carray{name: string, age: int, meta: list\u003cstring\u003e}\u003e\u003e\n$personEither = t\\decode(\n    value: $json,\n    with: t\\fromJson($libraryDefinition),\n)\n\n// Option data type from whsv26/functional\n// $person is Option\u003carray{name: string, age: int, meta: list\u003cstring\u003e}\u003e\n$personOption = t\\cast(\n    value: $json,\n    to: t\\fromJson($libraryDefinition),\n);\n```\n\n### Builtin type atomics\n\n##### mixed()\nRepresents value of any possible type.\n\n##### null()\nRepresents type for null value.\nSuitable for nullable types.\n```php\n$nullOrInt = union(null(), int())\n```\n\n##### int()\nRepresents integer number.\n\n##### positiveInt()\nRepresents positive integer number.\n\n##### float()\nRepresents number with floating point.\n\n##### numeric()\nRepresents either integer or float numbers.\n\n##### numericString()\nLike `numeric()` but represents also string numbers.\n\n#### bool()\nRepresents boolean value.\n\n#### string()\nRepresents string value.\n\n#### nonEmptyString()\nRepresents string that cannot be empty.\n\n#### scalar()\nAny scalar value.\n\n#### arrKey()\nRepresents array key (int | string)\n\n#### datetime()\nRepresents decoder that can create `DateTimeImmutable` from string.\nIt uses the constructor of `DateTimeImmutable` by default.\n\nYou can specify a format, and then the decoder will be use `DateTimeImmutable::createFromFormat`:\n```php\n$datetime = datetime(fromFormat: 'Y-m-d H:i:s');\n```\n\nIt uses UTC timezone by default.\nYou can pass different time zone during decoder instantiation:\n```php\n$datetime = datetime(timezone: 'Moscow/Europe');\n```\n\n### Generic types\n\n##### union(T1, T2, T3)\nRepresents type whose value will be of a single type out of multiple types.\n\n```php\n// int | string\n$intOrString = union(int(), string());\n// float | null\n$floatOrNull = union(float(), null());\n// int | float | string | null\n$intOrFloatOrStringOrNull = union($intOrString, $floatOrNull);\n```\n\n##### arrayOf(TK, TV)\nRepresents `array` with keys of type `TK` and values of type `TV`.\n\n```php\n// array\u003cint, string\u003e\n$arr = arrayOf(int(), string());\n```\n\n##### nonEmptyArrayOf(TK, TV)\nRepresents `non-empty-array` with keys of type `TK` and values of type `TV`.\n\n```php\n// non-empty-array\u003cint, string\u003e\n$nonEmptyArr = nonEmptyArrayOf(int(), string());\n```\n\n##### listOf(TV)\nRepresents `list` with values of type `TV`.\n\n```php\n// list\u003cstring\u003e\n$list = listOf(string());\n```\n\n##### nonEmptyListOf(TV)\nRepresents `non-empty-list` with values of type `TV`.\n\n```php\n// non-empty-list\u003cstring\u003e\n$list = nonEmptyListOf(string());\n```\n\n##### shape(prop1: T, prop2: T, propN: T)\nRepresents `array` with knows keys.\n\n```php\n// array{prop1: int, prop2: string, prop3: bool}\n$shape = shape(\n    prop1: int(),\n    prop2: string(),\n    prop3: bool(),\n);\n```\n\n##### partialShape(prop1: T, prop2: T, propN: T)\nLike `shape` represents `array` with knows keys, but each key is possibly undefined.\n\n```php\n// array{prop1?: int, prop2?: string, prop3?: bool}\n$shape = partialShape(\n    prop1: int(),\n    prop2: string(),\n    prop3: bool(),\n);\n```\n\n##### intersection(T1, T2, T3)\nDecoder that allows to combine multiple `shape` or `partialShape` into the one.\n\n```php\n// array{prop1: string, prop2: string, prop3?: string, prop4?: string}\n$intersection = intersection(\n    shape(\n        prop1: string(),\n        prop2: string(),\n    ),\n    partialShape(\n        prop3: string(),\n        prop4: string(),\n    ),\n);\n```\n\n##### tuple(T1, T2, T3)\nRepresents array that indexed from zero with fixed items count.\n\n```php\n// array{int, string, bool}\n$tuple = tuple(int(), string(), bool());\n```\n\n##### object(SomeClass::class)(prop1: T1, prop2: T2, propN: TN)\nAllows to create decoder for existed class. For each parameter of the constructor, you must explicitly specify a corresponding decoder.\n```php\nfinal class SomeClass\n{\n    public function __construct(\n        public int $prop1,\n        public string $prop2,\n    ) {}\n    \n    /**\n     * @return DecoderInterface\u003cSomeClass\u003e\n     */\n    public static function type(): DecoderInterface\n    {\n        return object(self::class)(\n            prop1: int(),\n            prop2: string(),\n        );\n    }\n}\n```\n\n##### partialObject(SomeClass::class)(prop1: T1, prop2: T2, propN: T3)\nLike `object` decoder, but each parameter of the constructor must be nullable.\n\n##### rec(fn() =\u003e T)\nRepresents recursive type. Only objects can be recursive.\n\n```php\nfinal class SomeClass\n{\n    /**\n     * @param list\u003cSomeClass\u003e $recursive\n     */\n    public function __construct(\n        public int $prop1,\n        public string $prop2,\n        public array $recursive = [],\n    ) { }\n\n    /**\n     * @return DecoderInterface\u003cSomeClass\u003e\n     */\n    public static function type(): DecoderInterface\n    {\n        $self = rec(fn() =\u003e self::type());\n\n        return object(self::class)(\n            prop1: int(),\n            prop2: string(),\n            recursive: listOf($self),\n        );\n    }\n}\n```\n\n##### fromJson(T)\nCombinator for decoder of type `T` which will be parsed from json representation.\n\n```php\n$shapeFromJson = fromJson(\n    shape(\n        prop1: string(),\n        prop2: string(),\n    )\n);\n```\n\n### Higher order helpers\n\n##### optional\nAllows you to mark property as possibly undefined.\n\n```php\n$personD = shape(\n    name: string(),\n    additional: listOf(string())-\u003eoptional(),\n);\n\n// inferred type: array{name: string, additional?: list\u003cstring\u003e}\n$firstShape = tryCast(['name' =\u003e 'foo'], $personD);\n\n// No additional field\n// ['name' =\u003e 'foo']\nprint_r($firstShape);\n\n// inferred type: array{name: string, additional?: list\u003cstring\u003e}\n$secondShape = tryCast(['name' =\u003e 'foo', 'additional' =\u003e ['bar']], $personD);\n\n// ['name' =\u003e 'foo', 'additional' =\u003e ['bar']]\nprint_r($secondShape);\n```\n\n##### default\nAllows you to define a fallback value if an untrusted source does not present one.\n\n```php\n$personD = shape(\n    name: string(),\n    isEmployed: bool()-\u003edefault(false),\n);\n\n// inferred type: array{name: string, isEmployed: bool}\n$firstShape = tryCast(['name' =\u003e 'foo'], $personD);\n\n// With default ['isEmployed' =\u003e false]\n// ['name' =\u003e 'foo', 'isEmployed' =\u003e false]\nprint_r($firstShape);\n\n// inferred type: array{name: string, isEmployed: bool}\n$secondShape = tryCast(['name' =\u003e 'foo', 'isEmployed' =\u003e true], $personD);\n\n// ['name' =\u003e 'foo', 'isEmployed' =\u003e true]\nprint_r($secondShape);\n```\n\n##### constrained\nAll decoders additionally can be constrained.\n\n```php\n$personD = shape(\n    name: string()-\u003econstrained(\n        minSize(is: 1),\n        maxSize(is: 255),\n    ),\n    street: string()-\u003econstrained(\n        minSize(is: 1),\n        maxSize(is: 255),\n    ),\n);\n```\n\n[List of builtin constraints](#constraints)\n\n##### from\nHelper method `from` is defined for each decoder.\nIt allows you to specify a path for a result property or rename one.\n\n```php\n$personD = shape(\n    name: string()-\u003efrom('$.person'),\n    street: string()-\u003efrom('$.address.street'),\n);\n\n$untrustedData = [\n    'person' =\u003e 'foo',\n    'address' =\u003e [\n        'street' =\u003e 'bar',\n    ],\n];\n\n// Inferred type: array{name: string, street: string}\n$personShape = tryCast($untrustedData, $personD);\n\n/* Decoded data looks different rather than source: [\n    'name' =\u003e 'foo',\n    'street' =\u003e 'bar',\n] */\nprint_r($personShape);\n```\n\nThe `$` sign means root of object. You can use just `$` when you want to change decoded structure nesting:\n\n```php\n$messengerD = shape(\n    kind: string()-\u003efrom('$.messenger_type'),\n    contact: string()-\u003efrom('$.messenger_contact'),\n);\n\n$personD = shape(\n    name: string()-\u003efrom('$.person'),\n    street: string()-\u003efrom('$.address.street'),\n    messenger: $messengerD-\u003efrom('$'), // means \"use the same data for this decoder\"\n);\n\n$untrustedData = [\n    'person' =\u003e 'foo',\n    'address' =\u003e [\n        'street' =\u003e 'bar',\n    ],\n    'messenger_type' =\u003e 'telegram',\n    'messenger_contact' =\u003e '@Klimick',\n];\n\n// inferred type: array{name: string, street: string, messenger: array{kind: string, messenger: string}}\n$personShape = tryCast($untrustedData, $personD);\n\n/* Decoded data looks different rather than source: [\n    'name' =\u003e 'foo',\n    'street' =\u003e 'bar',\n    'messenger' =\u003e [\n        'kind' =\u003e 'telegram',\n        'contact' =\u003e '@Klimick',\n    ]\n] */\nprint_r($personShape);\n```\n\n### Constraints\n\nConstraints can be attached to decoder with the [constrained](#constrained) higher order helper.\n\n##### equal (all types)\nChecks that a numeric value is equal to the given one.\n\n```php\n$fooString = string()\n    -\u003econstrained(equal('foo'));\n```\n\n##### greater (int, float, numeric)\nChecks that a numeric value is greater than the given one.\n\n```php\n$greaterThan10 = int()\n    -\u003econstrained(greater(10));\n```\n\n##### greaterOrEqual (int, float, numeric)\nChecks that a numeric value is greater or equal to the given one.\n\n```php\n$greaterOrEqualTo10 = int()\n    -\u003econstrained(greaterOrEqual(10));\n```\n\n##### less (int, float, numeric)\nChecks that a numeric value is less than the given one.\n\n```php\n$lessThan10 = int()\n    -\u003econstrained(less(10));\n```\n\n##### lessOrEqual (int, float, numeric)\nChecks that a numeric value is less or equal to the given one.\n\n```php\n$lessOrEqualTo10 = int()\n    -\u003econstrained(lessOrEqual(10));\n```\n\n##### inRange (int, float, numeric)\nChecks that a numeric value is in the given range\n\n```php\n$from10to20 = int()\n    -\u003econstrained(inRange(10, 20));\n```\n\n##### minLength (string, non-empty-string)\nChecks that a string value size is not less than given one.\n\n```php\n$min10char = string()\n    -\u003econstrained(minLength(10));\n```\n\n##### maxLength (string, non-empty-string)\nChecks that a string value size is not greater than given one.\n\n```php\n$max10char = string()\n    -\u003econstrained(maxLength(10));\n```\n\n##### startsWith (string, non-empty-string)\nChecks that a string value starts with the given value.\n\n```php\n$startsWithFoo = string()\n    -\u003econstrained(startsWith('foo'));\n```\n\n##### endsWith (string, non-empty-string)\nChecks that a string value ends with the given value.\n\n```php\n$endsWithFoo = string()\n    -\u003econstrained(endsWith('foo'));\n```\n\n##### uuid (string, non-empty-string)\nChecks that a string value is a valid UUID.\n\n```php\n$uuidString = string()\n    -\u003econstrained(uuid());\n```\n\n##### trimmed (string, non-empty-string)\nChecks that a string value has no leading or trailing whitespace.\n\n```php\n$noLeadingOrTrailingSpaces = string()\n    -\u003econstrained(trimmed());\n```\n\n##### matchesRegex (string, non-empty-string)\nChecks that a string value matches the given regular expression.\n\n```php\n$stringWithNumbers = string()\n    -\u003econstrained(matchesRegex('/^[0-9]{1,3}$/'));\n```\n\n##### forall (array\u003carray-key, T\u003e)\nChecks that the given constraint holds for all elements of an array value.\n\n```php\n$allNumbersGreaterThan10 = forall(greater(than: 10));\n\n$numbersGreaterThan10 = listOf(int())\n    -\u003econstrained($allNumbersGreaterThan10);\n```\n\n##### exists (array\u003carray-key, T\u003e)\nChecks that the given constraint holds for some elements of an array value.\n\n```php\n$hasNumbersGreaterThan10 = exists(greater(than: 10));\n\n$withNumberGreaterThan10 = listOf(int())\n    -\u003econstrained($hasNumbersGreaterThan10);\n```\n\n##### inCollection (array\u003carray-key, T\u003e)\nChecks that an array value contains a value equal to the given one.\n\n```php\n$listWith10 = listOf(int())\n    -\u003econstrained(inCollection(10));\n```\n\n##### maxSize (array\u003carray-key, T\u003e)\nChecks that an array value size is not greater than the given one.\n\n```php\n$max10numbers = listOf(int())\n    -\u003econstrained(maxSize(is: 10));\n````\n\n##### minSize (array\u003carray-key, T\u003e)\nChecks that an array value size is not less than the given one.\n\n```php\n$atLeast10numbers = listOf(int())\n    -\u003econstrained(minSize(is: 10));\n````\n\n##### allOf (any type)\nConjunction of all constraints.\n\n```php\n$from100to200 = allOf(\n    greaterOrEqual(to: 100),\n    lessOrEqual(to: 200),\n);\n\n$numbersFrom100to200 = listOf(int())\n    -\u003econstrained($from100to200);\n```\n\n##### anyOf (any type)\nDisjunction of all constraints.\n\n```php\n$from100to200 = allOf(\n    greaterOrEqual(to: 100),\n    lessOrEqual(to: 200),\n);\n\n$from300to400 = allOf(\n    greaterOrEqual(to: 300),\n    lessOrEqual(to: 400),\n);\n\n$numbersFrom100to200orFrom300to400 = listOf(int())\n    -\u003econstrained(anyOf($from100to200, $from300to400));\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fklimick%2Fdecode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fklimick%2Fdecode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fklimick%2Fdecode/lists"}