The short answer is that, as @FJRA noted in the comment, only certain types are supported. Which types? Enter the long answer.
Why the converter behaves as it does
Long answer: JSON supports only certain types, and their nested combinations, as defined e.g. here. Mathematica converter maps JSON objects to lists of rules, arrays to lists, strings to strings, plus has some special cases for True
, False
and Null
. Once Mathematica JSON converter sees a general expression, it does not know what to do with it.
The problem with the "obvious" solution to convert to string and store as a string is that there will be no automatic way (without imposing some additional conventions) to tell which strings are really strings and which are stringified Mathematica expressions. So, IMO, the converter is doing the right thing.
Digging deeper
You can actually quite easily trace the execution the the functions of interest. If we use my debug
function (from here), as
debug@Export[
Environment["USERPROFILE"] <> "\\AppData\\Local\\test.json",
HoldComplete[{1, 2, 3}], "JSON"]
It will quickly tell us to look at the function System`Convert`JSONDump`iexportJSON
, which in turn points to System`Convert`JSONDump`toString
. Inspecting the DownValues
of the latter, you will see the procedure I described above.
Making the JSON import - export more liberal (for illustration purposes only !)
If you really want to make the JSON import - export more liberal, so that, upon seeing an unrecognized general expression, it somehow converts it to string for export, and back to expression during import, here is one way:
ClearAll[withLiberalJsonTostring];
SetAttributes[withLiberalJsonTostring, HoldAll];
withLiberalJsonTostring[code_] :=
Block[{dv = DownValues[System`Convert`JSONDump`toString],
System`Convert`JSONDump`toString},
DownValues[System`Convert`JSONDump`toString] = Most[dv];
System`Convert`JSONDump`toString[expr_, _Integer] :=
StringJoin[
"StringifiedOpen",
StringReplace[ToString[FullForm@expr],
{"[" :> "EscapeOpen", "]" :> "EscapeClose", "," :> "EscapeComma"}],
"StringifyClose"
];
code];
and the import counterpart:
ClearAll[withLiberalJsonImport];
SetAttributes[withLiberalJsonImport, HoldAll];
withLiberalJsonImport[code_] :=
With[{result = code},
result /.
s_String :>
StringReplace[
s,
{"EscapeOpen" :> "[", "EscapeClose" :> "]", "EscapeComma" :> ","}
] /.
s_String /; StringMatchQ[s, "StringifiedOpen" ~~ __ ~~ "StringifyClose"] :>
ToExpression@ StringReplace[s, "StringifiedOpen" | "StringifyClose" :> ""]
];
Note that the escaping strings are arbitrary, and this will break if these particular strings are also used in different capacities in the JSON expression.
With this, we can do:
withLiberalJsonTostring[
Export[Environment["USERPROFILE"] <> "\\AppData\\Local\\test.json",
{1, 2, HoldComplete[{1, 2, 3}]},
JSON"]]
and
withLiberalJsonImport@
Import[Environment["USERPROFILE"] <> "\\AppData\\Local\\test.json", "JSON"]
(*
==> {1, 2, HoldComplete[{1, 2, 3}]}
*)
Note that I don't really recommend this method as robust, just posted this code for an illustration, and to aid the understanding of the matter. This is not robust on many grounds, incuding dependence on implementation details, the escaping procedure being arbitrary and not really robust, etc.
A robust solution would be to write an alternative converter (importer / exporter) to JSON, which would do the thing you want, and use that intead. Also, please have a look at the solution by @celtschk, which is a lot cleaner and simpler.
EDIT
As @celtschk pointed out in the comments, escaping is not really necessary if we add extra string quatation marks. The mechanism to distinguish strings from stringified expressions (to be converted back to expressions during import) is still needed however.