GitHub Actions fromJSON unable to parse, and what it really says
GitHub Actions fromJSON unable to parse is a phrase that does not appear in the function that raises these failures, which matters because searching for it finds discussions of other software instead. The runner has two JSON parsing paths, they produce two different wordings, and both of them lead with the same four words.

What this error means
An expression containing the JSON parsing function fails while it is being evaluated. Where the value is feeding a matrix, the annotation names a job that never started. Where it is feeding a condition, an action input or an environment value, the failure lands on the step, and the message is followed by a fragment of the text that could not be parsed. The most confusing reports are the ones where the same workflow worked yesterday, which is the signature of a value that is computed rather than written down.
Error parsing fromJsonWhat the function actually raises
The implementation is short enough to read in a minute and it settles the question. It takes the single parameter, converts it to a string, and then branches on whether strict JSON parsing is switched on.
On the strict path it uses the newer parser and, if that throws, raises a new exception whose message is the four words about parsing the function, then a colon, then the message from the underlying parser. So a strict failure carries a detail about what was wrong with the text.
On the other path it uses the older reader and, if that throws, raises an exception whose message is exactly those four words and nothing else, with the underlying reader exception attached as the inner one. So the text you see may be four words with the real detail in a nested exception rather than on the line you are reading.
Nowhere in either branch is there a phrase about being unable to parse. The runner comparison code, which currently runs both implementations and checks that they agree, spells the difference out in a comment: the older path throws the reader exception about reading a token, the newer one wraps it with the four words, and either may then be wrapped again in a template validation message. It calls out the empty string case by name as one where the two are expected to differ in text while agreeing in outcome.
| Parsing path | Message you see | Where the detail is |
|---|---|---|
| Strict parsing on | the four words, a colon, a detail | on the same line |
| Strict parsing off | the four words alone | in the inner exception |
| Either, inside a matrix | prefixed by the job being evaluated | after the position |
| Either, wrapped | prefixed by a template validation sentence | after the prefix |
Common causes
The value is an empty string
The single commonest input. An undeclared job output, a step output that was never written, or a context entry that does not exist all resolve to nothing, and nothing is not valid JSON.
The text uses single quotes or unquoted keys
Both are natural to type and neither is JSON. This is what hand written literals and shell string concatenation produce, and it reads as correct to almost everybody.
The value lost its tail in the step output file
That file is read line by line, so a value printed across several lines arrives truncated. A fragment of JSON is usually invalid JSON, so the failure looks like a syntax problem rather than a transport one.
A command wrote something other than JSON into the value
A warning on standard output, a progress line, or a shell prompt escape mixed into the captured text. In our experience redirecting only what you want is more reliable than trying to clean it afterwards.
How to fix it
Search for the four words, not the paraphrase
The fragment about parsing the function is what the runner emits. The sentence about an unexpected character belongs to the JSON library underneath and will return results from unrelated applications.
Print the value before it reaches the expression
- Add a step that echoes the value the expression will receive.
- Look first for whether it is empty, which is the commonest case by far.
- If it has content, pipe it through a JSON tool that exits non zero on invalid input.
echo "raw: [${{ needs.gen.outputs.value }}]"
echo '${{ needs.gen.outputs.value }}' | jq -e . > /dev/nullEmit JSON with a tool and ask for compact output
Generating JSON with a tool removes the quoting and comma mistakes at the source, and compact output keeps the whole value on one line so it survives the step output file intact.
echo "matrix=$(jq -c . config.json)" >> "$GITHUB_OUTPUT"Default to a value that parses
An empty array or an empty object is valid JSON and produces a defined result. This converts a confusing parse failure into an ordinary empty case that the rest of your workflow can reason about.
echo "targets=${TARGETS:-[]}" >> "$GITHUB_OUTPUT"The phrase that sends people to the wrong software
A wording about an unexpected character encountered while parsing a value circulates attached to this failure. That sentence is real, but it belongs to a widely used .NET JSON library rather than to Actions, and it appears in bug reports for all sorts of applications that happen to use it.
We checked. Searched as an exact phrase it returns thousands of results, and the ones we fetched and searched really do contain it, in desktop applications, game mod managers, API clients and documentation tools. None of the ones we looked at was a workflow annotation. A nonsense control phrase returned nothing, so the search was working.
That is enough to say something useful without overclaiming. The sentence is a genuine message from the library underneath, so it can plausibly reach you as inner detail on the strict path, and it is not a message Actions produces on its own. Searching for it will bury you in other people applications, and the four words about parsing the function are the fragment worth searching instead.
Where the bad value came from
The function is strict about what JSON is, and the values people feed it are usually assembled rather than written. Single quotes are not JSON. Unquoted keys are not JSON. A trailing comma is not JSON. All three are natural to write by hand and all three come out of string concatenation in a shell.
The empty string deserves separate mention because it is the commonest input of all and the least expected. A step output that was never written, a job output that was never declared, or a context entry that does not exist all resolve to an empty string, and an empty string is not valid JSON. The failure then blames the expression rather than the thing that failed to produce a value.
The fix is to stop building JSON by hand. A tool that emits JSON will emit valid JSON, and asking it for compact output keeps the value on one line, which matters when it has to pass through the step output file. Where the value can legitimately be absent, default it to an empty array or an empty object, both of which parse.
If the value is feeding a matrix specifically, the failure has its own shape and its own messages, and our page on a matrix that will not expand walks through them.
# not JSON, in three different ways
{os: ubuntu-latest}
{'os': 'ubuntu-latest'}
{"os": "ubuntu-latest",}
# emitted by a tool, compact, with a valid default
value=$(jq -cn '{os: ["ubuntu-latest"]}')
echo "value=${value:-{}}" >> "$GITHUB_OUTPUT"Why there is no recorded run on this page
The message this page is about is four words long on one of its two paths, and a capture of a run showing four words would add nothing to the four words quoted here. The parts that vary, the inner exception and the prefix, depend on which parsing path a given runner is taking, and that is not something we can pin down by recording one run on one day.
Reading the implementation is the better route and it is short. Both branches, both messages and the placement of the detail are visible in a single file, and the comparison code that documents the divergence between the two implementations is in another. Between them they say more than a screenshot of one outcome.
Nothing here is repairable either. A value that is not JSON is not a transient condition, and a runner that guessed at what the JSON was meant to be would be inventing your configuration.
How to prevent it
- Never build JSON by string concatenation in a shell step.
- Always emit compact JSON when a value has to cross the step output file.
- Give every JSON valued output a default that parses.
- Echo the value on the consuming side and keep that step permanently.
Frequently asked questions
Does GitHub Actions ever say "unable to parse" for fromJSON?
Why does the message sometimes have a detail and sometimes not?
Where does "unexpected character encountered while parsing value" come from?
What is the most common bad input?
Related guides
References
- actions/runner: FromJson.cs, both parsing branches and the message each raises
- actions/runner: PipelineTemplateEvaluatorWrapper.cs, the documented divergence between the two implementations
- GitHub Actions: expressions, the fromJSON function
- GitHub Actions: workflow commands, setting a step output
- GitHub Actions documentation