Shopify LiquidDoc is a {% doc %} block at the top of a Shopify snippet or static theme block that declares its parameters with JSDoc-style @param {type} name lines. Once it is there, Theme Check and the Shopify Liquid editor tooling warn when a render or content_for tag leaves out a required argument, passes one the file does not declare, or passes a literal of the wrong type.
Without it, a misspelt argument is invisible. Liquid’s render tag accepts any named argument you give it, and a snippet reading a variable nobody passed just gets nil and renders nothing. Shopify’s own LiquidDoc reference uses exactly this example: {% render 'loading-spinner', produt: product, show_vendorr: true %} raises no error anywhere.
What does a Shopify LiquidDoc header look like?
Here is a product card snippet with one required parameter and three optional ones. Optional names go in square brackets, the same convention JSDoc uses.
{% doc %}
Renders a product card with title, price and an optional vendor line.
@param {product} product - The product to show
@param {boolean} [show_vendor] - Print the vendor under the title
@param {number} [image_width] - Rendered image width in pixels, defaults to 600
@param {string} [heading_tag] - Heading element, defaults to h3
@example
{% render 'product-card', product: product, show_vendor: true %}
{% enddoc %}
{%- liquid
assign width = image_width | default: 600
assign tag = heading_tag | default: 'h3'
-%}
<div class="product-card">
{%- if product.featured_image -%}
{{ product.featured_image | image_url: width: width | image_tag: loading: 'lazy' }}
{%- endif -%}
<{{ tag }} class="product-card__title">{{ product.title }}</{{ tag }}>
{%- if show_vendor -%}
<p class="product-card__vendor">{{ product.vendor }}</p>
{%- endif -%}
<p class="product-card__price">{{ product.price | money }}</p>
</div>
The doc tag renders nothing on the storefront. Liquid parses what is inside it but does not execute it, so the header costs you nothing at runtime.
Three annotations are supported. Free text before the first @ is the description (or use @description explicitly), and only the first description shows on hover. @param takes an optional type, a required name and an optional description after a hyphen. @example can appear more than once, and each one shows up in the hover card when someone points at 'product-card' inside a render tag.
Optional does not mean defaulted. LiquidDoc has no default-value syntax, so the | default: filters in the liquid block are still doing the work. Writing the default into the parameter description is the closest you get.
Which types can a @param use?
The four basic types are string, number, boolean and object. On top of those, the ValidDocParamTypes check accepts any Liquid object name that is not exclusively a global, so {product}, {color}, {image} and {currency} are all valid. Add [] for a typed array, as in {product[]}. Two levels ({string[][]}) fails, and so does anything invented: {integer} is reported as “The parameter type ‘integer’ is not supported.” The type is optional, and @param some_untyped is fine.
Two rules govern names. Each name must be unique (UniqueDocParamNames). And a name cannot be a Liquid literal: @param {string} empty gets you “The parameter name ‘empty’ is reserved because Liquid parses it as a literal.” from ReservedDocParamNames, because {{ empty }} in the snippet body means the special empty value, not your argument.
What does Theme Check report once the header is there?
We ran shopify theme check from Shopify CLI 4.9.4 against a scratch theme containing the snippet above and a section with these calls:
{% render 'product-card', produt: item, show_vendorr: true %}
{% render 'product-card', product: item, image_width: '600' %}
The first line produced three offences: MissingRenderSnippetArguments (“Missing required argument ‘product’ in render tag for snippet ‘product-card’.”) and two UnrecognizedRenderSnippetArguments, one each for produt and show_vendorr. The second produced ValidRenderSnippetArgumentTypes: “Type mismatch for argument ‘image_width’: expected number, got string”. A snippet in the same theme with no {% doc %} header was rendered with a misspelt argument and got no offence at all. The checks read the header and nothing else.
The for and with forms count. {% render 'product-card' for collections.all.products as product %} and {% render 'product-card' with collections.all.products.first as product %} both satisfy the required product parameter.
Static blocks get the same treatment through content_for. Give blocks/badge.liquid a header with @param {string} text and @param {string} [tone], then render it from a section:
{% content_for 'block', type: 'badge', id: 'new-badge', text: 'New', colour: 'red' %}
That reports UnrecognizedContentForArguments for colour. Leave out text and you get MissingContentForArguments; pass text: 5 and ValidContentForArgumentTypes says it expected a string. (If theme blocks versus section blocks is still a live question on your build, our comparison of the two covers when a static block is the right tool.)
The header itself is checked too. UnusedDocParam warns when a declared parameter never appears in the file, and UnsupportedDocTag errors when {% doc %} appears anywhere other than snippets/ or blocks/. The check’s documentation page says snippets only, but the message Theme Check prints is “The doc tag can only be used within a snippet or block.”, and a header in blocks/ passed cleanly.
Where LiquidDoc type checking stops
It checks literals, not values. The ValidRenderSnippetArgumentTypes page says a variable argument always passes, and the LiquidDoc reference says the types of objects and variables are not validated. In our run, product: 'shirt' against a {product} parameter raised nothing.
Booleans are looser still. Every Liquid value is truthy or falsy, so a {boolean} parameter accepts anything, and show_vendor: 'no' passed the check. It also prints the vendor, because a non-empty string is truthy in Liquid. Pass true or false, never a string.
Dynamic snippet names switch the usage checks off entirely. {% assign name = 'price' %}{% render name %} cannot be resolved, so nothing is validated.
Why LiquidDoc warnings do not fail the build by default
Look at the severities. In theme-check:recommended, the header checks that catch a broken declaration (ValidDocParamTypes, UniqueDocParamNames, ReservedDocParamNames, UnsupportedDocTag) are errors. Every check that looks at a call site (the Missing, Unrecognized and Valid…ArgumentTypes checks for both render and content_for) is a warning, as is UnusedDocParam.
shopify theme check exits non-zero only for offences at or above --fail-level, and the default is error. Our scratch theme, with four argument offences on one section, exited 0. With --fail-level warning it exited 1.
If you want a misspelt argument to block a merge without failing on every other warning in an older theme, raise just those checks in .theme-check.yml:
extends: theme-check:recommended
MissingRenderSnippetArguments:
severity: error
UnrecognizedRenderSnippetArguments:
severity: error
ValidRenderSnippetArgumentTypes:
severity: error
MissingContentForArguments:
severity: error
UnrecognizedContentForArguments:
severity: error
ValidContentForArgumentTypes:
severity: error
With that file in place the same theme exits 1 at the default fail level, while an unrelated UndefinedObject warning stays a warning. The rest of the CI wiring (fail levels, baselines for legacy offences, the asset checks the preset leaves off) is in our Theme Check CI guide.
Should you add LiquidDoc to an existing theme?
Yes, starting with the snippets that take the most arguments and get rendered from the most places: product cards, price blocks, icon and image wrappers. Those are where a renamed parameter silently breaks a page nobody is looking at. A header is a few lines per file and changes nothing at runtime, so there is no risk in doing it piecemeal.
Write the types honestly. {product} on a parameter that sometimes receives a search result is a wrong annotation Theme Check cannot catch, because variables are never type-checked, and the next developer will believe it. Use {object} or leave the type off.
Then turn the call-site checks up to error in CI. Until you do, every argument offence is a warning and the pipeline passes with it.
We would not bother on a theme you are about to replace, or on one-argument snippets rendered from a single section, where the header is longer than the bug it prevents.
We can add LiquidDoc headers and a strict Theme Check config as part of a Shopify theme build or audit. If your theme has grown a snippets/ folder nobody quite trusts, our Shopify development team can take a look.