Your types compile and LINE still returns 400. This catches it before you send.
No dependencies at runtime.
A LINE Flex Message is JSON. Types will hold the shape together, but the failures collect in the places types cannot reach.
types catch structure, required keys, value kinds
types miss how large the JSON turns out to be
keys LINE has never heard of
how it looks on someone else's phone
And there are two kinds of failure, which is the awkward part.
One kind you notice immediately. LINE returns 400 and the send fails.
The other kind you never notice. The send succeeds. The delivery count goes up. On the recipient's screen the text has dissolved into the background, or an image slot is blank. No error comes back. Nothing lands in a log. It shows up only as silence, so running the thing in production will not tell you.
import { validate, format } from "flex-guard";
const result = validate(message);
if (!result.ok) {
for (const finding of result.errors) console.log(format(finding));
}[error] $.appMeta FlexMessage has no property "appMeta"
LINE rejects a message containing properties it does not know. If you attach
your own metadata, strip it just before sending — on every send path you have.
[error] $.contents.footer.contents[0].action.data PostbackAction.data exceeds 300 characters (400)
Are you putting the action itself in here? Carry an identifier and read the
payload back from somewhere else, and the limit stops mattering.
[warning] $.contents.body.contents[0].color A near-white text colour (#FFFFFF) is set explicitly
LINE adapts only the default text colour to the background. Setting a colour
opts out of that, so it dissolves for anyone in dark mode.
Live demo — paste a message, watch what comes back.
It runs this repository's build output, not a rewrite for the page (npm run demo:build generates it; CI checks the two still match). The page is static and talks to no server once it has loaded.
This split is the decision the whole library is built around.
| Meaning | What happens if you ignore it | |
|---|---|---|
error |
LINE will not accept it | The send fails. You find out at once |
warning |
The send succeeds | It looks wrong on their screen. You never find out |
result.ok reflects errors only. Warnings leave it true.
That is deliberate: it must not block something LINE would happily deliver. Fold the two together and you hand people a reason to switch the check off to silence a warning — and then the errors stop being seen too.
| Rule | |
|---|---|
schema/unknown-property |
A key that is not in the specification |
schema/missing-required |
A required key is absent (altText, a video's previewUrl) |
schema/unknown-type |
A type value the specification does not define |
schema/invalid-enum |
large where size wanted mega |
size/bubble-too-large |
A bubble over 10KB |
size/carousel-too-large |
A carousel over 50KB |
size/property-too-long |
Past a documented character limit — 300 for an action's data, 2000 for an image url |
schema/not-serializable |
Cannot become JSON at all (a cycle) |
| Rule | |
|---|---|
render/dark-mode-invisible |
Near-white text that dissolves in dark mode |
render/empty-container |
An empty box: no content, but the spacing stays |
render/mixed-bubble-size |
Bubbles of differing widths inside one carousel |
render/insecure-url |
A non-https image, which leaves the slot blank |
variables/unescaped-placeholder |
A substituted value that could break the JSON |
The list of permitted properties is generated from LINE's own published OpenAPI definition.
npm run spec:generateThe generated src/spec.ts carries the source URL, the date it was
fetched, and the sha256 of what was fetched. Which version of the specification you
are checking against is visible in the file itself.
It is generated rather than typed out because a false positive is the worst thing this library can do. Stop a valid message and nobody runs the check again. Copy six hundred property names by hand and you will get one of them wrong.
When LINE adds a property before the table catches up, there is a way through:
validate(message, { allowProperties: ["newProperty"] });If you store Flex and send it later, the declared type and the actual content can
drift apart. Send Flex JSON while the type still says text and LINE treats it as a
string — the recipient sees the raw JSON in their chat.
Nothing errors. The send succeeds and the delivery count goes up. An admin preview usually renders from the content rather than the declared type, so there it still looks correct. From your side, everything appears fine.
import { looksLikeFlex } from "flex-guard";
const detected = looksLikeFlex(body);
if (detected.looksLikeFlex) {
throw new Error(`about to send this as text (${detected.reason})`);
}Testing only for a leading { misses anything that starts as an array. Loosen it too
far and you block ordinary copy like [FORM_1] to get started. This parses the string
and looks for the shape of a Flex container.
npm install flex-guardNode 22.18 or later. Accepts either a whole {type:"flex", altText, contents} or a bare
bubble / carousel.
validate(message, {
disable: ["render"], // prefix match, so a whole family goes at once
allowProperties: ["myMeta"], // keys outside the specification you mean to send
});disable exists because a warning is sometimes exactly what was intended — white text
paired with a background colour, for instance.
It does not build anything. This checks; it does not assemble. The official SDK builds Flex, and how you assemble it is your business.
It does not send. It sits in front of your send call and stays out of it.
It carries no rule it cannot cite. A maximum length for altText, a maximum number
of bubbles in a carousel, a maximum nesting depth — no published figure for any of them
turned up, so none of them are here. One rule you cannot justify costs you the
credibility of the rest. They go in when a source does.
The same reasoning keeps render/insecure-url a warning. LINE's docs say plainly, of
image and video messages, to "make sure the URLs have the HTTPS (TLS 1.2 or later)
protocol" — but the Flex pages do not repeat it, and nothing found says the API refuses
one. What can be observed is an empty slot arriving on someone's screen; the send itself
goes through. Calling it an error would assert that LINE rejects the message, and there
is no published line behind that. It moves up when a source says so.
It cannot promise how anything looks. LINE states plainly that the same Flex Message renders differently depending on device OS, LINE version, resolution, language settings and font. What is here is the part that can be judged from the JSON.
npm testNode built-ins only. Types are stripped at run time, so the tests need no build step.
Each test fixes that only the one thing that was broken gets reported, because a
false positive costs more than a miss. That objects like styles and background are
never mistaken for components is pinned down the same way.
MIT. See LICENSE.