decodeFromCapData = jsonEncoded => {
    if (!isObject(jsonEncoded)) {
      // primitives pass through
      return jsonEncoded;
    }
    if (isArray(jsonEncoded)) {
      return jsonEncoded.map(encodedVal => decodeFromCapData(encodedVal));
    } else if (hasQClass(jsonEncoded)) {
      const qclass = jsonEncoded[QCLASS];
      typeof qclass === 'string' ||
        Fail`invalid ${q(QCLASS)} typeof ${q(typeof qclass)}`;
      switch (qclass) {
        // Encoding of primitives not handled by JSON
        case 'undefined': {
          return undefined;
        }
        case 'NaN': {
          return NaN;
        }
        case 'Infinity': {
          return Infinity;
        }
        case '-Infinity': {
          return -Infinity;
        }
        case 'bigint': {
          const { digits } = jsonEncoded;
          typeof digits === 'string' ||
            Fail`invalid digits typeof ${q(typeof digits)}`;
          return BigInt(digits);
        }
        case '@@asyncIterator': {
          // Deprecated qclass. TODO make conditional
          // on environment variable. Eventually remove, but after confident
          // that there are no more supported senders.
          //
          return Symbol.asyncIterator;
        }
        case 'symbol': {
          const { name } = jsonEncoded;
          return passableSymbolForName(name);
        }
        case 'tagged': {
          const { tag, payload } = jsonEncoded;
          return makeTagged(tag, decodeFromCapData(payload));
        }
        case 'slot': {
          // See note above about how the current encoding cannot reliably
          // distinguish which we should call, so in the non-default case
          // both must be the same and it doesn't matter which we call.
          const decoded = decodeRemotableFromCapData(
            jsonEncoded,
            decodeFromCapData,
          );
          // BEWARE: capdata does not check that `decoded` is
          // a promise or a remotable, since that would break some
          // capdata clients. We are deprecating capdata, and these clients
          // will need to update before switching to smallcaps.
          return decoded;
        }
        case 'error': {
          const decoded = decodeErrorFromCapData(
            jsonEncoded,
            decodeFromCapData,
          );
          if (passStyleOf(decoded) === 'error') {
            return decoded;
          }
          throw Fail`internal: decodeErrorFromCapData option must return an error: ${decoded}`;
        }
        case 'hilbert': {
          const { original, rest } = jsonEncoded;
          hasOwnPropertyOf(jsonEncoded, 'original') ||
            Fail`Invalid Hilbert Hotel encoding ${jsonEncoded}`;
          // Don't harden since we're not done mutating it
          const result = { [QCLASS]: decodeFromCapData(original) };
          if (hasOwnPropertyOf(jsonEncoded, 'rest')) {
            const isNonEmptyObject =
              typeof rest === 'object' &&
              rest !== null &&
              ownKeys(rest).length >= 1;
            if (!isNonEmptyObject) {
              throw Fail`Rest encoding must be a non-empty object: ${rest}`;
            }
            const restObj = decodeFromCapData(rest);
            // TODO really should assert that `passStyleOf(rest)` is
            // `'copyRecord'` but we'd have to harden it and it is too
            // early to do that.
            !hasOwnPropertyOf(restObj, QCLASS) ||
              Fail`Rest must not contain its own definition of ${q(QCLASS)}`;
            defineProperties(result, getOwnPropertyDescriptors(restObj));
          }
          return result;
        }
        // @ts-expect-error This is the error case we're testing for
        case 'ibid': {
          throw Fail`The capData protocol no longer supports ${q(QCLASS)} ${q(
            qclass,
          )}`;
        }
        default: {
          throw assert.fail(
            X`unrecognized ${q(QCLASS)} ${q(qclass)}`,
            TypeError,
          );
        }
      }
    } else {
      assert(typeof jsonEncoded === 'object' && jsonEncoded !== null);
      const decodeEntry = ([name, encodedVal]) => {
        typeof name === 'string' ||
          Fail`Property ${q(name)} of ${jsonEncoded} must be a string`;
        return [name, decodeFromCapData(encodedVal)];
      };
      const decodedEntries = entries(jsonEncoded).map(decodeEntry);
      return fromEntries(decodedEntries);
    }
  }
`decodeFromCapData` may rely on `jsonEncoded` being the result of a
plain call to JSON.parse. However, it *cannot* rely on `jsonEncoded`
having been produced by JSON.stringify on the output of `encodeToCapData`
above, i.e., `decodeFromCapData` cannot rely on `jsonEncoded` being a
valid marshalled representation. Rather, `decodeFromCapData` must
validate that.

@param {Encoding} jsonEncoded must be hardened