A small, self-contained JSON parser and serializer for Standard ML, built
on the sml-parsec parser
combinator library. sml-parsec is vendored (its source is committed under
lib/), so there are no external dependencies to install or fetch — clone the
repo and make. Parses to a simple algebraic data type, serializes back to
minified or pretty-printed JSON, and ships with a tiny jsonfmt CLI.
Verified TDD-style on both MLton and Poly/ML.
- Full RFC 8259 value grammar:
null, booleans, numbers, strings, arrays, objects. - Strict number grammar: rejects leading zeros (
01), explicit plus (+1), bare fractions (.5), and trailing dots (1.). Integers and reals are kept distinct (JIntvsJReal) so42round-trips as42, not42.0. - Arbitrary-precision integers:
JIntcarries anIntInf.int, so large values (ids, millisecond timestamps, …) parse losslessly and identically under MLton and Poly/ML — noOverflowcrash on values past MLton's fixed-widthintrange (~2^31). UseasIntto narrow to a machineintwhen it fits. - Escape-aware string parsing:
\" \\ \/ \b \f \n \r \tand\uXXXX(Basic Multilingual Plane; surrogate-pair decoding is a documented follow-up). Raw control characters inside strings are rejected. - Whole-input parsing with precise line/column error reporting.
- Serialization with correct re-escaping and JSON-correct signs (
-3, not the SML~3), in both compact and indented forms.
datatype json =
JNull
| JBool of bool
| JInt of IntInf.int (* JSON integer, arbitrary precision (any size) *)
| JReal of real (* JSON number with a fraction/exponent *)
| JStr of string
| JArr of json list
| JObj of (string * json) listNote: json contains a real, so it has no derived equality. For comparing
parsed values in tests, use a structural comparison with a tolerance on
JReal (see test/test.sml's jsonEq).
Breaking change: JInt now carries IntInf.int (was int), so integers of
any size parse losslessly and identically across compilers. Code that
pattern-matched JInt n and used n as a machine int should switch to
asInt, or convert explicitly with IntInf.toInt / IntInf.fromInt.
From structure Json:
val parseJson : string -> json CharParsec.result
val asInt : json -> int optionparseJson skips leading whitespace, parses one JSON value, and requires
end-of-input (trailing garbage is an error). asInt narrows a JInt to a
machine int, returning NONE for a non-integer or a value outside this
compiler's Int range. The result of parseJson is CharParsec's result:
datatype 'a result = Ok of 'a | Err of errorOn Err e, CharParsec.errorToString e produces a human-readable message with
line and column.
From structure JsonPretty:
val toString : json -> string (* compact / minified *)
val toStringIndent : int -> json -> string (* pretty, n-space indent *)Both guarantee their output re-parses to a structurally equal value.
open Json
val Ok v = parseJson "{ \"name\": \"ml\", \"nums\": [1, 2.5, -3] }"
(* v = JObj [("name", JStr "ml"),
("nums", JArr [JInt 1, JReal 2.5, JInt ~3])] *)
val compact = JsonPretty.toString v
(* {"name":"ml","nums":[1,2.5,-3]} *)
val pretty = JsonPretty.toStringIndent 2 v
(* {
"name": "ml",
"nums": [
1,
2.5,
-3
]
} *)Reads JSON from stdin, validates it, and writes it back out.
make fmt # builds bin/jsonfmt (MLton)
echo '{"b":2,"a":[1,2,3]}' | bin/jsonfmt # pretty (2-space indent)
echo '{"b":2,"a":[1,2,3]}' | bin/jsonfmt -c # compact / minified
echo '{"b":2,"a":[1,2,3]}' | bin/jsonfmt -i 4 # pretty, 4-space indentExits non-zero and prints a parse error to stderr on invalid input.
Requires MLton and/or Poly/ML.
make test # build + run the suite under MLton
make test-poly # run the suite under Poly/ML
make all-tests # both
make fmt # build the jsonfmt CLI (MLton only)
make cleansml-json/
sml.pkg smlpkg manifest (documents the upstream dep)
Makefile MLton + Poly/ML targets
.github/workflows/ci.yml CI: MLton + Poly/ML
lib/github.com/sjqtentacles/sml-parsec/ vendored sml-parsec (committed)
src/
json.sml datatype json + parseJson
jsonPretty.sml JsonPretty: minified + indented serializers
json.mlb basis + vendored parsec.mlb + src files
test/
test.mlb
test.sml dependency-free check-based suite
bin/
jsonfmt.sml CLI (MLton-only)
jsonfmt.mlb
This repo vendors sml-parsec under
lib/github.com/sjqtentacles/sml-parsec/ (committed, no network required to
build). sml.pkg records the upstream dependency
(require github.com/sjqtentacles/sml-parsec) for users who prefer
smlpkg sync, but the build uses the committed copy.
MIT.