简体   繁体   中英

How to lookup the value for a key in type Json (fp-ts)?

I can extract a value from a json string in an Option with this code:

Code that works

import { identity, pipe } from "fp-ts/lib/function";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/lib/Option";
import * as R from "fp-ts/lib/Record";

const jsonString = '{"a": 1}';
const a = pipe(
  jsonString,
  (x) =>
    E.tryCatch(
      () => JSON.parse(x),
      (reason) => new Error(String(reason))
    ),
  O.fromEither,
  O.chain(R.lookup("a"))
);
console.log(a);

The type coming out of the E.tryCatch call is Either<Error, any> and R.lookup seems to accept any .

Code that doesn't work

Instead of using JSON.parse directly, I would like to useparse from the Json module in fp-ts .

import { identity, pipe } from "fp-ts/lib/function";
import * as J from "fp-ts/lib/Json";
import * as O from "fp-ts/lib/Option";
import * as R from "fp-ts/lib/Record";
const jsonString = '{"a": 1}';
const a = pipe(
  jsonString,
  J.parse,
  O.fromEither,
  O.chain(R.lookup("a"))  <-- Error
);

Trying to use =R.lookup= on =Json= gives the following error:

Argument of type '<A>(r: Record<string, A>) => Option<A>' 
is not assignable to parameter of type '(a: Json) => Option<Json>'

I guess it is because Json can have lots of values ( boolean | number | string | null | JsonArray | JsonRecord ) and only JsonRecord would be compatible with Record .

Question

How can I convert Json to Record (or anything letting me "lookup" value for key)? Or perhaps narrow my Json object to JsonRecord ?

What I tried

Following the zipObject example for fromFoldableMap , I tried to do something like below without success:

import { identity, pipe } from "fp-ts/lib/function";
import * as J from "fp-ts/lib/Json";
import * as O from "fp-ts/lib/Option";
import * as R from "fp-ts/lib/Record";
const jsonString = '{"a": 1}';
const a = pipe(
  jsonString,
  J.parse,
  O.fromEither,
  O.map((kvs) => R.fromFoldableMap(last<J.Json>(), R.Foldable)(kvs, identity)), <-- Not working
  O.chain(R.lookup("a"))
);
 

The reason there's an error is because the Json type isn't necessarily assignable to a Record . For example, the string '11' would be valid input to JSON.parse or J.parse and would result in the number 11 . That number cannot be used like a Record . It could likewise be an array, or a string, or null, etc. You'll need to further parse the returned value before you can use it with R.lookup .

The most straightforward thing I can think of to solve that problem is to add io-ts * into the mix.

Simplest I suspect is something like:

import { pipe } from "fp-ts/lib/function";
import * as J from "fp-ts/lib/Json";
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import * as R from "fp-ts/lib/Record";
import * as t from "io-ts";

const jsonString = '{"a": 1}';
const a = pipe(
  jsonString,
  J.parse,
  E.chainW(t.UnknownRecord.decode), // Different Error type so chainW
  O.fromEither,
  O.chain(R.lookup("a"))
);

If you're going to leverage io-ts and you know the rough shape of the object you're parsing, then you can improve on this approach with something like:

import { pipe } from "fp-ts/lib/function";
import * as J from "fp-ts/lib/Json";
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import * as t from "io-ts";

const jsonString = '{"a": 1}';
// Here you can define the exact shape of what you
// think J.parse will have returned.
const schema = t.type({
  a: t.number
});
const a = pipe(
  jsonString,
  J.parse,
  // Decode will then match that shape or return a `E.left` if it fails to.
  E.chainW(schema.decode),
  O.fromEither,
  // The type of innerA is number and no need to do `lookup`
  // because the entire shape of the object has been validated
  O.map(({ a: innerA }) => innerA)
);

For absolute completeness just using fp-ts I think you could get something working like:

import { pipe } from "fp-ts/lib/function";
import * as J from "fp-ts/lib/Json";
import * as R from "fp-ts/lib/Record";
import * as O from "fp-ts/lib/Option";

const jsonString = '{"a": 1}';
// Note that the return type here is going to be `Option<Json>` which
// may still be difficult to work with in your code for the same reason
// that you couldn't use `R.lookup` directly. Parsing with `io-ts` will
// give you easier types to work with.
const a = pipe(
  jsonString,
  J.parse,
  O.fromEither,
  O.chain((json) => {
    // This is basically just inlining what `io-ts` would do for you so
    // I again would recommend that library.
    if (typeof json === "object" && json !== null && !(json instanceof Array)) {
      return pipe(json, R.lookup("a"));
    } else {
      return O.none;
    }
  })
);

The last thing I'll mention is that, if you really want this function to work with as little changes as possible and you're ok with the O.Option<J.Json> return type, and if you can assert that the string will always be a JSON object, and you really don't want to use io-ts , you could achieve what you want with a type assertion like:

import { pipe } from "fp-ts/lib/function";
import * as J from "fp-ts/lib/Json";
import * as O from "fp-ts/lib/Option";
import * as R from "fp-ts/lib/Record";
const jsonString = '{"a": 1}';
const a = pipe(
  jsonString,
  J.parse,
  O.fromEither,
  // Again this is unsafe because the value might be an Array or a primitive
  // value. You're basically just escaping from the type system.
  O.map((json: J.Json): J.JsonRecord => json as J.JsonRecord),
  O.chain(R.lookup("a"))
);

But I don't recommend this approach as I think it will be difficult to work with the return value and you open yourself up to errors if the input json string isn't an object.


* io-ts is a library from the same people that make fp-ts that is used for validating the shape of unknown data in TypeScript.

You could just parse the string right when you get it and use the parsed object in your program .

const jsonString = '{"a": 1}';
var parsedJsonString = JSON.parse(jsonString);

console.log( "Parsed JSON: ", parsedJsonString );
console.log( "a:", parsedJsonString.a );

To be a little bit safer, you could check if it is really a json string like this:

function isJsonString(jsonString){
    try {
        JSON.parse(jsonString);
    } catch (e) {
        return false;
    }
    return true;
}

const jsonString = '{"a": 1}';
var isValidJson = isJsonString( jsonString );
if( isValidJson ){
    var parsedJsonString = JSON.parse(jsonString);

    console.log( "Parsed JSON: ", parsedJsonString );
    console.log( "a:", parsedJsonString.a );
}else{
    console.log("jsonString is not a valid json");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM