UnknownProperty
UnknownProperty
Severity: error | Type: LiquidHtml
This check reports reading a property that a value provably does not have — a typo, a field the GraphQL query did not select, or a key the partial you called never returns.
It only speaks when it can prove the shape of a value. Where it cannot — a dynamic key, a value built by a filter it cannot see through, an argument it cannot resolve — it stays silent. An absent offense is therefore not proof that a property exists; when the check does report, it is because the shape it proved does not contain the property you read.
Where the shape comes from
- platformOS objects —
contextand everything under it, from the docset the linter ships (refresh it withpos-cli check update-docs). - GraphQL results — the selection sets of the
.graphqlfile a{% graphql %}tag names. Fragment spreads (...record) and inline fragments (... on Record) are resolved, including transitively. Fields selected conditionally with@include(if:)/@skip(if:)are resolved against the argument values at the call site. List fields answerfirst,last,sizeand numeric indexes. - Hash literals —
{% assign settings = { "theme": "dark" } %}andparse_json. - Partial return values —
{% function %}analyzes the partial it calls, with your arguments bound, so a shape crosses the partial boundary.{% include %}shares the caller's scope and is not a boundary. - Writes —
{% assign %},{% hash_assign %}and{% function %}writing at a path ({% hash_assign user['role'] = 'admin' %}) narrow a shape that is already known.
Examples
✗ Incorrect Code Example (Avoid using this):
A typo on a platformOS object:
{{ context.curren_user.name }}
curren_user is a typo — the correct property is current_user.
A typo on a GraphQL result:
{% graphql result = 'records/search' %}
{{ result.records.result[0].id }}
result is a typo — the correct property is results.
A field the query did not select. Given this query, where author is selected only when $include_author is true:
query find($id: ID!, $include_author: Boolean = false) {
records(per_page: 1, filter: { id: { value: $id } }) {
results {
...post
author: related_record(table: ["profile"], join_on_property: "author_id") @include(if: $include_author) {
id
}
}
}
}
fragment post on Record {
id
created_at
}
and this partial, which forwards the flag and returns one record:
{% comment %}app/views/partials/lib/queries/posts/find.liquid{% endcomment %}
{% liquid
graphql r = 'posts/find', id: id, include_author: include_author
return r.records.results.first
%}
a caller that does not ask for the author cannot read it:
{% liquid
function post = 'lib/queries/posts/find', id: context.params.id
assign author_id = post.author.id
%}
Unknown property 'author' on 'post'. — the query declares $include_author: Boolean = false, the caller sent nothing, so @include(if: $include_author) excluded the field. Reading post.created would be reported too: created_at comes from the post fragment, and created is not in it.
✓ Correct Code Example (Use this instead):
{{ context.current_user.name }}
{% graphql result = 'records/search' %}
{{ result.records.results[0].id }}
{% comment %}Ask for the field you intend to read:{% endcomment %}
{% liquid
function post = 'lib/queries/posts/find', id: context.params.id, include_author: true
assign author_id = post.author.id
%}
For the query above, the same read is judged by what the call site passes:
| Call site | post.author |
|---|---|
include_author: true |
accepted |
include_author: false |
reported — the field is excluded |
| argument omitted | reported — the query declares Boolean = false |
| forwarded from a value the linter cannot resolve | accepted — unprovable, so nothing is claimed |
author absent from the query altogether |
reported |
@skip(if:) behaves as the inverse of @include(if:).
When the check stays silent
By design, so that it does not report code it cannot understand:
- A variable whose value it cannot see into — the result of a filter it does not model, or a partial that mutates its arguments through an alias.
- A dynamic property (
{{ record[field_name] }}) or a write to a dynamic key. - A property written in one branch of a conditional and read after it — a write inside
{% if %}is not treated as a fact about the code that follows. - An empty hash:
{ "errors": {} }is a placeholder a partial fills later, not a hash with no keys, so anything may be read undererrors. - A conditional GraphQL field whose flag it cannot resolve.
- A custom GraphQL scalar such as
HashObject, which holds a hash rather than a primitive. - An array whose item shape is unknown —
sizeis answered, individual items are not.
Configuration
The default configuration for this check:
UnknownProperty:
enabled: true
severity: error
Disabling This Check
An offense usually means the property is genuinely absent, so prefer fixing the read — or the query, or the arguments you pass — over silencing it. The check already withholds judgment wherever it cannot prove a shape.
When you do need to silence one, scope it to the line rather than the file. A platformos-check-disable comment applies until the matching platformos-check-enable, or to the end of the file if there is none — and one written without a check name silences every check, not just this one.
{% liquid
# platformos-check-disable UnknownProperty
assign value = record[dynamic_key]
# platformos-check-enable UnknownProperty
%}