diff --git a/src/BUILD b/src/BUILD
index fe7bb28cc5a23b..88acc042fcec9f 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -449,6 +449,7 @@ filegroup(
"//src/main/java/com/google/devtools/common/options:srcs",
"//src/main/java/net/starlark/java/cmd:srcs",
"//src/main/java/net/starlark/java/spelling:srcs",
+ "//src/main/java/net/starlark/java/lib/json:srcs",
"//src/main/native:srcs",
"//src/main/protobuf:srcs",
"//src/main/tools:srcs",
diff --git a/src/main/java/net/starlark/java/lib/json/BUILD b/src/main/java/net/starlark/java/lib/json/BUILD
new file mode 100644
index 00000000000000..bb76f94bebea5f
--- /dev/null
+++ b/src/main/java/net/starlark/java/lib/json/BUILD
@@ -0,0 +1,21 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+licenses(["notice"])
+
+filegroup(
+ name = "srcs",
+ srcs = glob(["**"]),
+ visibility = ["//src:__subpackages__"],
+)
+
+# Starlark json module
+java_library(
+ name = "json",
+ srcs = ["Json.java"],
+ visibility = ["//src/main/java/net/starlark/java:clients"],
+ deps = [
+ "//src/main/java/net/starlark/java/annot",
+ "//src/main/java/net/starlark/java/eval",
+ "//src/main/java/net/starlark/java/syntax",
+ ],
+)
diff --git a/src/main/java/net/starlark/java/lib/json/Json.java b/src/main/java/net/starlark/java/lib/json/Json.java
new file mode 100644
index 00000000000000..a5fd566c8d65f5
--- /dev/null
+++ b/src/main/java/net/starlark/java/lib/json/Json.java
@@ -0,0 +1,618 @@
+// Copyright 2020 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.starlark.java.lib.json;
+
+import java.util.Arrays;
+import java.util.Map;
+import net.starlark.java.annot.Param;
+import net.starlark.java.annot.StarlarkBuiltin;
+import net.starlark.java.annot.StarlarkMethod;
+import net.starlark.java.eval.ClassObject;
+import net.starlark.java.eval.Dict;
+import net.starlark.java.eval.EvalException;
+import net.starlark.java.eval.Mutability;
+import net.starlark.java.eval.Starlark;
+import net.starlark.java.eval.StarlarkInt;
+import net.starlark.java.eval.StarlarkIterable;
+import net.starlark.java.eval.StarlarkList;
+import net.starlark.java.eval.StarlarkThread;
+import net.starlark.java.eval.StarlarkValue;
+import net.starlark.java.syntax.Location;
+
+// Tests at //src/test/java/net/starlark/java/eval:testdata/json.sky
+
+/**
+ * Json defines the Starlark {@code json} module, which provides functions for encoding/decoding
+ * Starlark values as JSON (https://tools.ietf.org/html/rfc8259).
+ */
+@StarlarkBuiltin(
+ name = "json",
+ category = "core.lib",
+ doc = "Module json is a Starlark module of JSON-related functions.")
+public final class Json implements StarlarkValue {
+
+ private Json() {}
+
+ /** Call {@code Starlark.addModule(env, Json.INSTANCE)} to add json to the environment. */
+ public static final Json INSTANCE = new Json();
+
+ /** An interface for StarlarkValue subclasses to define their own JSON encoding. */
+ public interface Encodable {
+ String encodeJSON();
+ }
+
+ /**
+ * Encodes a Starlark value as JSON.
+ *
+ *
An application-defined subclass of StarlarkValue may define its own JSON encoding by
+ * implementing the {@link Encodable} interface. Otherwise, the encoder tests for the {@link Map},
+ * {@link StarlarkIterable}, and {@link ClassObject} interfaces, in that order, resulting in
+ * dict-like, list-like, and struct-like encoding, respectively. See the Starlark documentation
+ * annotation for more detail.
+ *
+ *
Encoding any other value yields an error.
+ */
+ @StarlarkMethod(
+ name = "encode",
+ doc =
+ "
The encode function accepts one required positional argument, which it converts to"
+ + " JSON by cases:\n"
+ + "
\n"
+ + "
None, True, and False are converted to 'null', 'true', and 'false',"
+ + " respectively.\n"
+ + "
An int, no matter how large, is encoded as a decimal integer. Some decoders"
+ + " may not be able to decode very large integers.\n"
+ + "
A float is encoded using a decimal point or an exponent or both, even if its"
+ + " numeric value is an integer. It is an error to encode a non-finite "
+ + " floating-point value.\n"
+ + "
A string value is encoded as a JSON string literal that denotes the value. "
+ + " Each unpaired surrogate is replaced by U+FFFD.\n"
+ + "
A dict is encoded as a JSON object, in key order. It is an error if any key"
+ + " is not a string.\n"
+ + "
A list or tuple is encoded as a JSON array.\n"
+ + "
A struct-like value is encoded as a JSON object, in field name order.\n"
+ + "
\n"
+ + "An application-defined type may define its own JSON encoding.\n"
+ + "Encoding any other value yields an error.\n",
+ parameters = {@Param(name = "x")})
+ public String encode(Object x) throws EvalException {
+ Encoder enc = new Encoder();
+ try {
+ enc.encode(x);
+ } catch (StackOverflowError unused) {
+ throw Starlark.errorf("nesting depth limit exceeded");
+ }
+ return enc.out.toString();
+ }
+
+ private static final class Encoder {
+
+ private final StringBuilder out = new StringBuilder();
+
+ private void encode(Object x) throws EvalException {
+ if (x == Starlark.NONE) {
+ out.append("null");
+ return;
+ }
+
+ if (x instanceof String) {
+ appendQuoted((String) x);
+ return;
+ }
+
+ if (x instanceof Boolean || x instanceof StarlarkInt) {
+ out.append(x);
+ return;
+ }
+
+ // if (x instanceof StarlarkFloat) {
+ // if (!Double.isFinite(((StarlarkFloat) x).toDouble())) {
+ // throw Starlark.errorf("cannot encode non-finite float %s", x);
+ // }
+ // out.append(x.toString()); // always contains a decimal point or exponent
+ // return;
+ // }
+
+ if (x instanceof Encodable) {
+ // Application-defined Starlark value types
+ // may define their own JSON encoding.
+ out.append(((Encodable) x).encodeJSON());
+ return;
+ }
+
+ // e.g. dict (must have string keys)
+ if (x instanceof Map) {
+ Map, ?> m = (Map) x;
+
+ // Sort keys for determinism.
+ Object[] keys = m.keySet().toArray();
+ for (Object key : keys) {
+ if (!(key instanceof String)) {
+ throw Starlark.errorf(
+ "%s has %s key, want string", Starlark.type(x), Starlark.type(key));
+ }
+ }
+ Arrays.sort(keys);
+
+ // emit object
+ out.append('{');
+ String sep = "";
+ for (Object key : keys) {
+ out.append(sep);
+ sep = ",";
+ appendQuoted((String) key);
+ out.append(':');
+ try {
+ encode(m.get(key));
+ } catch (EvalException ex) {
+ throw Starlark.errorf(
+ "in %s key %s: %s", Starlark.type(x), Starlark.repr(key), ex.getMessage());
+ }
+ }
+ out.append('}');
+ return;
+ }
+
+ // e.g. tuple, list
+ if (x instanceof StarlarkIterable) {
+ out.append('[');
+ String sep = "";
+ int i = 0;
+ for (Object elem : (StarlarkIterable) x) {
+ out.append(sep);
+ sep = ",";
+ try {
+ encode(elem);
+ } catch (EvalException ex) {
+ throw Starlark.errorf("at %s index %d: %s", Starlark.type(x), i, ex.getMessage());
+ }
+ i++;
+ }
+ out.append(']');
+ return;
+ }
+
+ // e.g. struct
+ if (x instanceof ClassObject) {
+ ClassObject obj = (ClassObject) x;
+
+ // Sort keys for determinism.
+ String[] fields = obj.getFieldNames().toArray(new String[0]);
+ Arrays.sort(fields);
+
+ out.append('{');
+ String sep = "";
+ for (String field : fields) {
+ out.append(sep);
+ sep = ",";
+ appendQuoted(field);
+ out.append(":");
+ try {
+ Object v = obj.getValue(field); // may fail (field not defined)
+ encode(v); // may fail (unexpected type)
+ } catch (EvalException ex) {
+ throw Starlark.errorf("in %s field .%s: %s", Starlark.type(x), field, ex.getMessage());
+ }
+ }
+ out.append('}');
+ return;
+ }
+
+ throw Starlark.errorf("cannot encode %s as JSON", Starlark.type(x));
+ }
+
+ private void appendQuoted(String s) {
+ // We use String's code point iterator so that we can map
+ // unpaired surrogates to U+FFFD in the output.
+ // TODO(adonovan): if we ever get an isPrintable(codepoint)
+ // function, use uXXXX escapes for non-printables.
+ out.append('"');
+ for (int i = 0, n = s.length(); i < n; ) {
+ int cp = s.codePointAt(i);
+
+ // ASCII control code?
+ if (cp < 0x20) {
+ switch (cp) {
+ case '\b':
+ out.append("\\b");
+ break;
+ case '\f':
+ out.append("\\f");
+ break;
+ case '\n':
+ out.append("\\n");
+ break;
+ case '\r':
+ out.append("\\r");
+ break;
+ case '\t':
+ out.append("\\t");
+ break;
+ default:
+ out.append("\\u00");
+ out.append(HEX[(cp >> 4) & 0xf]);
+ out.append(HEX[cp & 0xf]);
+ }
+ i++;
+ continue;
+ }
+
+ // printable ASCII (or DEL 0x7f)? (common case)
+ if (cp < 0x80) {
+ if (cp == '"' || cp == '\\') {
+ out.append('\\');
+ }
+ out.append((char) cp);
+ i++;
+ continue;
+ }
+
+ // non-ASCII
+ if (Character.MIN_SURROGATE <= cp && cp <= Character.MAX_SURROGATE) {
+ cp = 0xFFFD; // unpaired surrogate
+ }
+ out.appendCodePoint(cp);
+ i += Character.charCount(cp);
+ }
+ out.append('"');
+ }
+ }
+
+ private static final char[] HEX = "0123456789abcdef".toCharArray();
+
+ /** Parses a JSON string as a Starlark value. */
+ @StarlarkMethod(
+ name = "decode",
+ doc =
+ "The decode function accepts one positional parameter, a JSON string.\n"
+ + "It returns the Starlark value that the string denotes.\n"
+ + "
"
+ + "
'null', 'true', and 'false' are parsed as None, True, and False.\n"
+ + "
Numbers are parsed as int or float, depending on whether they contain either"
+ + " a decimal point or an exponent.\n"
+ + "
a JSON object is parsed as a new unfrozen Starlark dict."
+ + " Keys must be unique strings.\n"
+ + "
a JSON array is parsed as new unfrozen Starlark list.\n"
+ + "
\n"
+ + "Decoding fails if x is not a valid JSON encoding.\n",
+ parameters = {@Param(name = "x")},
+ useStarlarkThread = true)
+ public Object decode(String x, StarlarkThread thread) throws EvalException {
+ return new Decoder(thread.mutability(), x).decode();
+ }
+
+ private static final class Decoder {
+
+ // The decoder necessarily makes certain representation choices
+ // such as list vs tuple, struct vs dict, int vs float.
+ // In principle, we could parameterize it to allow the caller to
+ // control the returned types, but there's no compelling need yet.
+
+ private final Mutability mu;
+ private final String s; // the input string
+ private int i = 0; // current index in s
+
+ private Decoder(Mutability mu, String s) {
+ this.mu = mu;
+ this.s = s;
+ }
+
+ // decode is the entry point into the decoder.
+ private Object decode() throws EvalException {
+ try {
+ Object x = parse();
+ if (skipSpace()) {
+ throw Starlark.errorf("unexpected character %s after value", quoteChar(s.charAt(i)));
+ }
+ return x;
+ } catch (StackOverflowError unused) {
+ throw Starlark.errorf("nesting depth limit exceeded");
+ } catch (EvalException ex) {
+ throw Starlark.errorf("at offset %d, %s", i, ex.getMessage());
+ }
+ }
+
+ // Returns a Starlark string literal that denotes c.
+ private static String quoteChar(char c) {
+ return Starlark.repr("" + c);
+ }
+
+ // parse returns the next JSON value from the input.
+ // It consumes leading but not trailing whitespace.
+ private Object parse() throws EvalException {
+ char c = next();
+ switch (c) {
+ case '"':
+ return parseString();
+
+ case 'n':
+ if (s.startsWith("null", i)) {
+ i += "null".length();
+ return Starlark.NONE;
+ }
+ break;
+
+ case 't':
+ if (s.startsWith("true", i)) {
+ i += "true".length();
+ return true;
+ }
+ break;
+
+ case 'f':
+ if (s.startsWith("false", i)) {
+ i += "false".length();
+ return false;
+ }
+ break;
+
+ case '[':
+ // array
+ StarlarkList