Categories
5 Minute Tricks Hard Skills Software Development

JSON and undefined

The other day, I discussed with a colleague how to create a mapped PATCH object, that we wanted to send down the network.

The issue was that we wanted to keep the code readable and easy to maintain, without loops, maps, and complex trickery. We used many libraries down the pipe, which complicated the issue, so below I only show you the bare minimum to give away the concept we talked about.

The whole object looked something like this (clearly, we like dogs, so we talked about dogs ):

const dog = {
    name: "Buddy",
    breed: "Golden Retriever",
    age: 3,
    color: "Golden",
    weight: 30, // in kilograms
    isVaccinated: true,
    favoriteToy: "Tennis Ball"
};

When you receive a request from the UI through a form, with a PATCH request, you will only find one or two properties sent, like:

const patchDog = {
    age: 4,
    weight: 32,
};

Technically, it is nice, but usually you do not send down properties in the backend as it is received from the browser, but you want to validate them, and to map it into a request object specific to the repository function, SQL query, API request for a different microservice, … you get the idea 🙂

When you do that, the simplest and most readable solution to do is just to map all the properties one by one to the new object.

const patchRequestObject = {
    age: patchDog.age,
    weight: patchDog.weight
};

But wait, how do you know that only these two properties will be updated? Well, you don’t, so you have to update all of the properties.

Note: Actually, if you only do one-to-one mapping, without any transformation and validation, you could just do what I have done above for all the properties, because of what you're going to learn below about undefined.

Just to have a simple complexity, here is a validation function we decided we have to call on each property, but only if it exists:

function validate(input) {
    return `${input} is valid`;
}

The above example would be something like this:

const patchRequestObject = {
    age: validate(patchDog.age),
    weight: validate(patchDog.weight)
};

It is better, but we have the same problem, we do not know what properties will come, so we might add it to all properties:

const patchRequestObject = {
    name: validate(patchDog.name),
    breed: validate(patchDog.breed),
    age: validate(patchDog.age),
    color: validate(patchDog.color),
    weight: validate(patchDog.weight),
    isVaccinated: validate(patchDog.isVaccinated),
    favoriteToy: validate(patchDog.favoriteToy)
};

In code, it might look good, but look at what it will generate:

{
  name: 'undefined is valid',
  breed: 'undefined is valid',
  age: '4 is valid',
  color: 'undefined is valid',
  weight: '32 is valid',
  isVaccinated: 'undefined is valid',
  favoriteToy: 'undefined is valid'
}

You probably guessed it, it does not yield the result we want. In our case, undefined is definitely not valid.

What we want is only the two properties that the user sent us through the form: the age and the weight.

There is a simple solution, which is to use undefined as a default value if there is no value sent to be patched.

You can implement it in the mapping object, like:

const patchRequestObject = {
    name: patchDog.name ? validate(patchDog.name) : undefined,
    breed: patchDog.breed ? validate(patchDog.breed) : undefined,
    age: patchDog.age ? validate(patchDog.age) : undefined,
    color: patchDog.color ? validate(patchDog.color) : undefined,
    weight: patchDog.weight ? validate(patchDog.weight) : undefined,
    isVaccinated: patchDog.isVaccinated ? validate(patchDog.isVaccinated) : undefined,
    favoriteToy: patchDog.favoriteToy ? validate(patchDog.favoriteToy) : undefined
};

It works, but it has become difficult to read, consequently making it hard to maintain. If you have a massive project in that style, you will likely find yourself rewriting the entire thing or seeking a new job.

Here is the output of the above:

{
  name: undefined,
  breed: undefined,
  age: '4 is valid',
  color: undefined,
  weight: '32 is valid',
  isVaccinated: undefined,
  favoriteToy: undefined
}

It did what we wanted, but we can improve the code, by only doing the job in the validate function itself. If it is repeated, and you have a single place where you can do it, be lazy, and do it in only one place!

Here is the refactored code:

function validate(input: any) {
    return input ? `${input} is valid` : undefined;
}

const patchRequestObject = {
    name: validate(patchDog.name),
    breed: validate(patchDog.breed),
    age: validate(patchDog.age),
    color: validate(patchDog.color),
    weight: validate(patchDog.weight),
    isVaccinated: validate(patchDog.isVaccinated),
    favoriteToy: validate(patchDog.favoriteToy)
};

Much better! Tap on the shoulders 🙂

The result is the same, with undefined values.

{
  name: undefined,
  breed: undefined,
  age: '4 is valid',
  color: undefined,
  weight: '32 is valid',
  isVaccinated: undefined,
  favoriteToy: undefined
}

You can argue that it’s nice and all, but still, you want to patch only the two properties that came from the UI. Will the repository not update all of the properties?

And the answer is, no, undefined will disappear, especially if you are sending it down the network to an API. If it were kept null, you would be right. Keep reading 🙂

Let’s look at what JSON.stringify does to our new object:

Input:

JSON.stringify({
  name: undefined,
  breed: undefined,
  age: '4 is valid',
  color: undefined,
  weight: '32 is valid',
  isVaccinated: undefined,
  favoriteToy: undefined
})

Output:

'{"age":"4 is valid","weight":"32 is valid"}'

When you receive it on the other and of the pipe and you JSON.parse it, that will be your object to use for the PATCH request:

JSON.parse('{"age":"4 is valid","weight":"32 is valid"}')

// outcome:
{
    age: '4 is valid',
    weight: '32 is valid'
}

If you had choose null instead of undefined, you would actually replace all of the existing properties with null values. Your customers would not be happy finding out they cannot find their dogs in the application anymore, because all the data they meant to keep, got replaced with null.

Here is the code for null, easy to miss, and expensive to fix (usually):

// PLEASE DO NOT USE NULL IN PRODUCTION IN THIS SCENARIO

var patchDog = {
    age: 4,
    weight: 32,
};

// note that here we return null instead of undefined
function validate(input) {
    return input ? `${input} is valid` : null;
}

var patchRequestObject = {
    name: validate(patchDog.name),
    breed: validate(patchDog.breed),
    age: validate(patchDog.age),
    color: validate(patchDog.color),
    weight: validate(patchDog.weight),
    isVaccinated: validate(patchDog.isVaccinated),
    favoriteToy: validate(patchDog.favoriteToy)
};

console.log(JSON.stringify(patchRequestObject))

Output:

{
    "name": null,
    "breed": null,
    "age": "4 is valid",
    "color": null,
    "weight": "32 is valid",
    "isVaccinated": null,
    "favoriteToy": null
}

Instead of:

{
    "age": "4 is valid",
    "weight": "32 is valid",
}

Here is the suggested, working code in one piece:

var patchDog = {
    age: 4,
    weight: 32,
};

// use undefined as default value
function validate(input) {
    return input ? `${input} is valid` : undefined;
}

var patchRequestObject = {
    name: validate(patchDog.name),
    breed: validate(patchDog.breed),
    age: validate(patchDog.age),
    color: validate(patchDog.color),
    weight: validate(patchDog.weight),
    isVaccinated: validate(patchDog.isVaccinated),
    favoriteToy: validate(patchDog.favoriteToy)
};

console.log(JSON.stringify(patchRequestObject))

With the expected outcome again:

{
    "age": "4 is valid",
    "weight": "32 is valid",
}

I hope you enjoyed and learned something today. If you have any suggestions, please leave a comment below.

Thank you for reading 🙂

By Botond Bertalan

I love programming and architecting code that solves real business problems and gives value for the end-user.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.