1

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 use parse 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"))
);
 
xav
  • 4,101
  • 5
  • 26
  • 32
  • 1
    Possibly helpful: [How to parse and validate a JsonRecord with io-ts?](https://stackoverflow.com/questions/65902287/how-to-parse-and-validate-a-jsonrecord-with-io-ts) – pilchard Jun 16 '22 at 08:33
  • Thanks for pointing this. I did read it yesterday but since a specific Json module was added to fp-ts, I still have the feeling that this problem can be solved without needing io-ts. – xav Jun 16 '22 at 09:25
  • Ah I posted my answer before seeing that someone else had mentioned `io-ts`. I went ahead and showed how to get things working without it in my answer, but I tried to make an argument for using that library. I hope my reasoning is clear – Souperman Jun 18 '22 at 04:46

2 Answers2

1

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.

Souperman
  • 5,057
  • 1
  • 14
  • 39
0

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");
}
Dr.Random
  • 430
  • 3
  • 16