mojotech / json-type-validation

TypeScript JSON type validation
MIT License
155 stars 15 forks source link

object() decoder needs a 'strict' option to disallow undefined fields #46

Open tohagan opened 4 years ago

tohagan commented 4 years ago

Just discovered this awesome library. Just what I was looking for! Thanks very much.

I had a look at the code for the object decoder and noted that it iterates over the fields of the fields of the object specification and returns as a result only these fields. That great for cleaning JSON input values, however I often need to check that fields in the input json object are only those defined by the object decoder. To support this I think we need a strict argument on the object() decoder that checks and reports an error if any additional fields are found in the json object.

  static object<A>(decoders?: DecoderObject<A>, strict?: boolean = false) {
    return new Decoder((json: unknown) => {
      if (isJsonObject(json) && decoders) {
        let obj: any = {};
        for (const key in decoders) {
          if (decoders.hasOwnProperty(key)) {
            const r = decoders[key].decode(json[key]);
            if (r.ok === true) {
              // tslint:disable-next-line:strict-type-predicates
              if (r.result !== undefined) {
                obj[key] = r.result;
              }
            } else if (json[key] === undefined) {
              return Result.err({message: `the key '${key}' is required but was not present`});
            } else {
              return Result.err(prependAt(`.${key}`, r.error));
            }
          }
        }
        // ADDED
        if (strict) {
          for (const key in json) {
            if (!decoders.hasOwnProperty(key)) {
              return Result.err({message: `an undefined key '${key}' is present in the object`});
            }
          }
        }
        return Result.ok(obj);
      } else if (isJsonObject(json)) {
        return Result.ok(json);
      } else {
        return Result.err({message: expectedGot('an object', json)});
      }
    });
  }
tohagan commented 4 years ago

Actually I think there a more functional approach that you'd probably recommend. We just add a strictObject(decoders) function that invokes object(decoders).

Here's my untested JavaScript (not TypeScript) version

const checkStrict = (decoders) => {
  return new Decoder((json) => {
    for (const key in json) {
      if (!decoders.hasOwnProperty(key)) {
        return Result.err({message: `an undefined key '${key}' is present in the object`});
      }
    }
    return Result.ok(json);
  });
}

// like object() but checks that only declared fields are used.
export const strictObject = (decoders) => object(decoders).andThen(checkStrict(decoders));
tohagan commented 4 years ago

Alas .. That won't work because you've made the Decoder() constructor private.

psiservices-rcocks commented 4 years ago

I'm also looking for such a feature. In keeping with the library perhaps there could be an only combinator, so a decoder could be defined as:

const decoder = only(object({a: string()})

where this decoder would throw on {a: "hello", b: "world"} but not {a: "hello world"}.