BigInt Type Error on new Node SDK

I am using an Express backend with the Node SDK. After updating to the latest version of the API, and version 9.0.0 of the Node SDK, my app can’t even list the catalog:

$ curl http://localhost:3001
{
  "message":"Do not know how to serialize a BigInt",
  "error":"TypeError: Do not know how to serialize a BigInt\n
    at JSON.stringify (<anonymous>)\n
    at stringify (node_modules/express/lib/response.js:1123:12)\n
    at ServerResponse.json (node_modules/express/lib/response.js:260:14)\n
    at ServerResponse.send (node_modules/express/lib/response.js:158:21)\n
    at _61d‍.r.router.get (routes/index.js:43:9)\n
    at process._tickCallback (internal/process/next_tick.js:68:7)"
}

Here is my repo in case anyone can take a look: GitHub - manda-farmer/square-express-shopping-cart-public

Steps to reproduce:

  1. Clone repo
    git clone https://github.com/manda-farmer/square-express-shopping-cart-public.git
  2. Login to your Square Dashboard and create an application with the latest version, 2021-02-26.
  3. Update package.json to use version 9.0.0
    "square": "^9.0.0"
  4. Install all dependencies
    npm install --save
  5. Send a curl request to the app:
    curl http://localhost:3001

Do I need to wrap all of my responses with something that converts BigInt to strings first, so that JSON.stringify/res.send can serialize them properly? If so, does anyone have any suggestions on how to go about this?

Per Release Version 9.0.0 · square/square-nodejs-sdk · GitHub

Endpoints that return 64-bit integers now return a BigInt object instead of a Number object.

P.S.
Sorry for the rough, unfinished documentation!

Update:

Do I need to wrap all of my responses with something that converts BigInt to strings first, so that JSON.stringify/res.send can serialize them properly? If so, does anyone have any suggestions on how to go about this?

Answer: Yes. In case anyone else runs into this, here is how I accomplished it:

  1. Import the json-bigint package
   import JSONBig from 'json-bigint';
  1. Then pass the response object that contains BigInt through the necessary JSONBig functions
  try {
    // Retrieves locations in order to display the store name
    const { result: { locations } } = await locationsApi.listLocations();
    // Get CatalogItem and CatalogImage object
    const { result: { objects } } = await catalogApi.listCatalog(undefined, types);
    // Returns the catalog and location IDs, since we don't need to
    // print the full locationInfo array
    res.json({
      locationId: locations.id,
      items: JSONBig.parse(JSONBig.stringify(new CatalogList(objects).items))
    });

I will be pushing this change to the repo, hopefully in a more elegant DRY implementation.

Thanks for reporting! We changed to BigInts due to 64-bit integer overflows in our JS clients, and anticipated some rough edges around the migration. Thanks for posting the solution here and feel free to reach out to us if there’s anything we can do to make this easier going forward.

This is from the quickstart back end for the React Native quickstart app. Any chance I can get a hand updating this code block so that it uses json-bigint? I am getting errors in Heroku and the ios Simulator when trying to createCustomerCard in the React Native app.

app.post('/createCustomerCard', async (request, response) => {
  const requestBody = request.body;
  console.log(requestBody);
  try {
    const createCustomerCardRequestBody = {
      cardNonce: requestBody.nonce
    };
    const customerCardResponse = await 
customersApi.createCustomerCard(requestBody.customer_id, 
createCustomerCardRequestBody);
    console.log(customerCardResponse.result.card);

    response.status(200).json(createPaymentResponse.result.payment);
  } catch (e) {
     console.log(
      `[Error] Status:${e.statusCode}, Messages: ${JSON.stringify(e.errors, null, 2)}`);

     sendErrorMessage(e.errors, response);
  }
});

Errors:
ios simulator


Heroku logs:
2021-03-20T23:38:33.851157+00:00 app[web.1]: > [email protected] start /app
2021-03-20T23:38:33.851158+00:00 app[web.1]: > node index.js
2021-03-20T23:38:33.851158+00:00 app[web.1]:
2021-03-20T23:38:34.734870+00:00 app[web.1]: Your app is listening on port 44636
2021-03-20T23:38:35.361890+00:00 heroku[web.1]: State changed from starting to up
2021-03-20T23:38:37.514978+00:00 app[web.1]: {
2021-03-20T23:38:37.514988+00:00 app[web.1]: customer_id: ‘JGS8WH3PPRT3D808FX977HZEM4’,
2021-03-20T23:38:37.514988+00:00 app[web.1]: nonce: ‘cnon:CBASELnSUOpJRGgvNRDOhLxHsKU’
2021-03-20T23:38:37.514988+00:00 app[web.1]: }
2021-03-20T23:38:38.125447+00:00 app[web.1]: {
2021-03-20T23:38:38.125460+00:00 app[web.1]: id: ‘ccof:sEVnVwiGTsVP1w3j3GB’,
2021-03-20T23:38:38.125490+00:00 app[web.1]: cardBrand: ‘VISA’,
2021-03-20T23:38:38.125493+00:00 app[web.1]: last4: ‘1111’,
2021-03-20T23:38:38.125494+00:00 app[web.1]: expMonth: 2n,
2021-03-20T23:38:38.125495+00:00 app[web.1]: expYear: 2038n
2021-03-20T23:38:38.125495+00:00 app[web.1]: }
2021-03-20T23:38:38.126389+00:00 app[web.1]: [Error] Status:TypeError: Do not know how to serialize a BigInt, Messages: undefined
2021-03-20T23:38:38.128560+00:00 app[web.1]: (node:23) UnhandledPromiseRejectionWarning: TypeError: Cannot read property ‘0’ of undefined
2021-03-20T23:38:38.128561+00:00 app[web.1]: at sendErrorMessage (/app/index.js:117:17)
2021-03-20T23:38:38.128562+00:00 app[web.1]: at /app/index.js:93:5
2021-03-20T23:38:38.128563+00:00 app[web.1]: at processTicksAndRejections (internal/process/task_queues.js:93:5)
2021-03-20T23:38:38.128563+00:00 app[web.1]: (Use node --trace-warnings ... to show where the warning was created)
2021-03-20T23:38:38.130003+00:00 app[web.1]: (node:23) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see Command-line options | Node.js v15.12.0 Documentation). (rejection id: 1)
2021-03-20T23:38:38.130157+00:00 app[web.1]: (node:23) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
2021-03-20T23:39:07.416018+00:00 heroku[router]: at=error code=H12 desc=“Request timeout” method=POST path="/createCustomerCard" host=shenanigans-app-server.herokuapp.com request_id=d3063c57-3656-4f3b-9c10-4da5caa0bf68 fwd=“76.121.9.110” dyno=web.1 connect=0ms service=30000ms status=503 bytes=0 protocol=https

I found this post because I was looking at “[Error] Status:TypeError: Do not know how to serialize a BigInt”

Anyway, this has been a blocker for me for a couple weeks so any help would be greatly appreciated!

@JeffBall My workaround is super ugly, but it should work. Make sure you install the required dependency, json-bigint, with your preferred JS package manager (i.e. yarn, npm, etc.)

import JSONBig from 'json-bigint';
app.post('/createCustomerCard', async (request, response) => {
  const requestBody = request.body;
  console.log(requestBody);
  try {
    const createCustomerCardRequestBody = {
      cardNonce: requestBody.nonce
    };
    const customerCardResponse = await 
customersApi.createCustomerCard(requestBody.customer_id, 
createCustomerCardRequestBody);
    console.log(customerCardResponse.result.card);
    paymentParsed = JSONBig.parse(JSONBig.stringify(createPaymentResponse.result.payment));
    response.status(200).json(paymentParsed);
  } catch (e) {
     console.log(
      `[Error] Status:${e.statusCode}, Messages: ${JSON.stringify(e.errors, null, 2)}`);

     sendErrorMessage(e.errors, response);
  }
});

For comparison, here is my version of the back-end payment capture in Express (I don’t have a front-end for it yet, but that’s pretty much the last step, and Square already has one I can use as a boilerplate.)

/**
 * Matches: POST /checkout/payment/
 *
 * Description:
 *  Takes the payment infomration that is submitted from the /checkout/payment page,
 *  then calls payment api to pay the order
 *
 *  You can learn more about the CreatePayment endpoint here:
 *  https://developer.squareup.com/reference/square/payments-api/create-payment
 *
 * Request Body:
 *  orderId: Id of the order to be updated
 *  locationId: Id of the location that the order belongs to
 *  idempotencyKey: Unique identifier for request from client
 *  nonce: Card nonce (a secure single use token) created by the Square Payment Form
 */
router.post("/payment", async (req, res, next) => {
  const {
    orderId,
    idempotencyKey,
    nonce
  } = req.body;
  try {
    // get the latest order information in case the price is changed from a different session
    const { result: { order } } = await ordersApi.retrieveOrder(orderId);
    if (order.totalMoney.amount > 0) {
      // Payment can only be made when order amount is greater than 0
      const orderRequestBody = {
        sourceId: nonce, // Card nonce created by the payment form
        idempotencyKey,
        amountMoney: order.totalMoney, // Provides total amount of money and currency to charge for the order.
        orderId: order.id, // Order that is associated with the payment
      };
    } else {
      // Settle an order with a total of 0.
      await ordersApi.payOrder(order.id, {
        idempotencyKey
      });
    }
    const { result: { payment } } = await paymentsApi.createPayment(orderRequestBody);
    const paymentParsed = JSONBig.parse(JSONBig.stringify(payment));
    res.json(
      {
          result: "Success! Order paid!",
          payment: paymentParsed
        })
  } catch (error) {
    next(error);
  }
});

Also, here are my clumsy-still-deep-in-development invoice routes which I use as a workaround for obtaining in-person payments on orders created through the API (since they only appear in POS/app after payment, otherwise)

/**
 * Matches: POST /checkout/create-invoice
 * 
 * Description:
 *   Create an invoice if the customer opts to pay later. This also allows
 *   for in-person payment capture via Square mobile app at the time of
 *   delivery, resulting in a lower fee. This method only creates the 
 *   invoice, it does not publish it yet. This allows for fully synchronous
 *   order confirmation with Square.
 * 
 *   You can learn more about the CreateInvoice endpoint here:
 *   https://developer.squareup.com/reference/square/invoices-api/create-invoice
 *
 * Request Body:
 *  orderId: Id of the order to create an invoice from
 *  locationId: Id of the location that the order belongs to
 *  idempotencyKey: Unique identifier for request from client
 */
router.post("/create-invoice", async (req, res, next) => {
  const {
    orderId
  } = req.body;
  
  // Function to set the due date for the invoice to a specific
  // day of the week
  function nextWeekdayDate(date, day_in_week) {
    const ret = new Date(date || new Date());
    ret.setDate(ret.getDate() + (day_in_week -1 - ret.getDay() + 7) % 7 + 1);
  return ret;
  }
  
  try {
    // Since deliveries happen every Tuesday, set the dueDate
    // to Tuesday using nextWeekdayDate()
    const date = new Date();
    // Set due date to next Tuesday
    const dueDate = new nextWeekdayDate(date, 2);
    const dueDateString = dueDate.toISOString().split("T")[0];
    let { result: { order } } = await ordersApi.retrieveOrder(orderId);
    const invoiceRequestBody = {
      invoice: {
        locationId: order.locationId,
        orderId: orderId,
        paymentRequests: [
        {
          // Pay balance in full, not installments
          requestType: 'BALANCE',
          // If you already have a delivery date set,
          // uncomment this line, and comment out
          // dueDate: dueDateString to use that value instead
          //dueDate: order.expectedShippedAt,
          dueDate: dueDateString,
          reminders: [
          {
            message: 'Your order is scheduled for tomorrow',
            relativeScheduledDays: -1
          }]
        }],
        deliveryMethod: 'SHARE_MANUALLY',
        idempotencyKey: randomBytes(45).toString("hex")
      }
    };
    //const { result: { invoice } } = await invoicesApi.createInvoice(invoiceRequestBody);
    //const invoiceParsed = JSONBig.parse(JSONBig.stringify(invoice));
    const cart = new Cart(orderId, invoiceRequestBody);
    order = await cart.info();
    invoice = await cart.invoiceCreate();
    res.json(
      {
          result: "Success! Invoice created!",
          invoice: invoice,
          order: order
        })
  } catch (error) {
    next(error);
  }
});

/**
 * Matches: POST /checkout/publish-invoice
 * 
 * Description:
 *  Takes the invoiceId from the invoice that was created, and then
 *  publishes it to generate a publicly-viewable URL for payment
 *  capture and/or review.
 *  
 *  You can learn more about the PublishInvoice endpoint here:
 *  https://developer.squareup.com/reference/square/invoices-api/publish-invoice
 * 
 * Request Body:
 *  invoiceId: Id of the invoice to publish
 *  version: The version of the invoice to publish
 *  idempotencyKey: Unique identifier for request from client
 */
router.post("/publish-invoice", async (req, res, next) => {
  const {
    invoiceId,
    orderId
  } = req.body;
  try {
    // get the full invoice object, to obtain the version
    let { result: invoice } = await invoicesApi.getInvoice(invoiceId);
    const invoiceRequestBody = {
      invoiceId: invoiceId,
      invoice: {
        version: invoice.version
      },
      idempotencyKey: randomBytes(45).toString("hex")
    };
    const cart = new Cart(orderId, invoiceRequestBody);
    invoice = cart.invoicePublish();
    order = cart.info();
    res.json(
    {
        result: "Success! Invoice published!",
        invoice: invoice,
        order: order
    })
  } catch (error) {
    next(error);
  }
  });

I hope this helps! Let me know if it doesn’t work for you
It through me straight through a loop, but I took it as a really solid opportunity to level up on my JS knowledge. Maybe someday, the TSC will decide that a native type (as of ES2020) should also get parsed natively.

1 Like

@mniaey Thanks! I got a 200 in my Heroku logs. That’s a relief.

Your code led the way, but I had to change it to .result.card in order to get the customer card response. See below.

app.post('/createCustomerCard', async (request, response) => {
  const requestBody = request.body;
  console.log(requestBody);
  try {
    const createCustomerCardRequestBody = {
      cardNonce: requestBody.nonce
    };
    const customerCardResponse = await customersApi.createCustomerCard(requestBody.customer_id, createCustomerCardRequestBody);

    console.log(customerCardResponse.result.card);

    customerCardResponseParsed = JSONBig.parse(JSONBig.stringify(customerCardResponse.result.card));

    response.status(200).json(customerCardResponseParsed);

  } catch (e) {
     console.log(
      `[Error] Status:${e.statusCode}, Messages: ${JSON.stringify(e.errors, null, 2)}`);

     sendErrorMessage(e.errors, response);
  }
});

I will look over your the other back end code that you posted too, especially what you are working on with the invoices routes.

Thanks again!

Glad to hear it! So now we have working fixes for React Native and Express with ES6-style imports. Hopefully other folks can jump in and add their fixes for any other frameworks here.

We can also use like, parsing the object in the given below function

    toObject(obj) {
        return JSON.parse(JSON.stringify(obj, (key, value) =>
            typeof value === 'bigint'
                ? value.toString()
                : value // return everything else unchanged
        ));
    }

For Example : Fetch list of all added cards

Solution : return cards.map(card => { card = toObject(card); });

Reference Taken From : JSON.stringify() doesn’t know how to serialize a BigInt

UnhandledPromiseRejectionWarning originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). A rejected promise is like an exception that bubbles up towards the application entry point and causes the root error handler to produce that output. It usually happens in async await functions, and there’s an easy fix.

const functionName = async (arguments) => {
  try {
  // Your code here
  } catch (error) {
  // Handle rejection here
  }
};

A nice way to wait for several Promises to resolve to use the Promise.all function. It expects an Array of Promises, and produces a Promise that resolves to an Array containing the values that the individual Promises resolved to. Furthermore, it only resolves after the last Promise resolves. If any of its input Promises rejects, then the entire Promise.all expression rejects as well. It effectively “runs” all of its input processes “at the same time”, emulating the classic “fork-join” pattern.

Hey there,

Its been a while since this was posted and you probably don’t watch here anymore, however thank you for this post.

I was running into this exact issue and this fixed it, so thank you. :heart: