Recursively convert an object fields from snake case to camelCase
Asked Answered
G

10

15

I had an object like this (fields in snake_case)

const obj = {
  vt_core_random: {
    user_details: {
      first_name: "xyz",
      last_name: "abc",
      groups: [
        {
          id: 1,
          group_type: "EXT"
        },
        {
          id: 2,
          group_type: "INT"
        }
      ],
      address_type: {
        city_name: "nashik",
        state: {
          code_name: "MH",
          name: "Maharashtra"
        }
      }
    }
  }
};

I want to recursily convert its fields to camelCase, so the expected output is given below

const obj = {
  vtCoreRandom: {
    userDetails: {
      firstName: "xyz",
      lastName: "abc",
      groups: [
        {
          id: 1,
          groupType: "EXT"
        },
        {
          id: 2,
          groupType: "INT"
        }
      ],
      addressType: {
        cityName: "LMN",
        state: {
          codeName: "KOP",
          name: "PSQ"
        }
      }
    }
  }
};

I tried using mapKeys() but I just can't wrap my head around the recursive part of this. any help is highly appreciated. Also I have the ability to use lodash if it makes the process any simpler

Glove answered 16/1, 2020 at 12:23 Comment(0)
G
30

You can use lodash's _.transform() to create a recursive function that iterates the keys and converts them camel case with _.camelCase(). Transform can also handle arrays, so if the iterated object (target) is an array, we don't need to change the keys.

const camelize = obj => _.transform(obj, (acc, value, key, target) => {
  const camelKey = _.isArray(target) ? key : _.camelCase(key);
  
  acc[camelKey] = _.isObject(value) ? camelize(value) : value;
});

const obj = {"vt_core_random":{"user_details":{"first_name":"xyz","last_name":"abc","groups":[{"id":1,"group_type":"EXT"},{"id":2,"group_type":"INT"}],"address_type":{"city_name":"nashik","state":{"code_name":"MH","name":"Maharashtra"}}}}};

const result = camelize(obj);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
Governorship answered 16/1, 2020 at 13:57 Comment(1)
Typescript version: import { camelCase, isArray, transform, isObject } from "lodash"; const camelize = (obj: Record<string, unknown>) => transform(obj, (result: Record<string, unknown>, value: unknown, key: string, target) => { const camelKey = isArray(target) ? key : camelCase(key); result[camelKey] = isObject(value) ? camelize(value as Record<string, unknown>) : value; });Mercator
H
9

Plain JavaScript

(works for snake_case and kebap-case)

const recursiveToCamel = item => {
  if (Array.isArray(item)) {
    return item.map(el => recursiveToCamel(el));
  } else if (typeof item === 'function' || item !== Object(item)) {
    return item;
  }
  return Object.fromEntries(
    Object.entries(item).map(([key, value]) => [
      key.replace(/([-_][a-z])/gi, c => c.toUpperCase().replace(/[-_]/g, '')),
      recursiveToCamel(value),
    ]),
  );
};

Here is a small demo using the object provided by the question:

const obj = {
  vt_core_random: {
    user_details: {
      first_name: "xyz",
      last_name: "abc",
      groups: [{
          id: 1,
          group_type: "EXT"
        },
        {
          id: 2,
          group_type: "INT"
        }
      ],
      address_type: {
        city_name: "nashik",
        state: {
          code_name: "MH",
          name: "Maharashtra"
        }
      }
    }
  }
};

const recursiveToCamel = item => {
  if (Array.isArray(item)) {
    return item.map(el => recursiveToCamel(el));
  } else if (typeof item === 'function' || item !== Object(item)) {
    return item;
  }
  return Object.fromEntries(
    Object.entries(item).map(([key, value]) => [
      key.replace(/([-_][a-z])/gi, c => c.toUpperCase().replace(/[-_]/g, '')),
      recursiveToCamel(value),
    ]),
  );
};

console.log(recursiveToCamel(obj));

TypeScript

const recursiveToCamel = (item: unknown): unknown => {
  if (Array.isArray(item)) {
    return item.map((el: unknown) => recursiveToCamel(el));
  } else if (typeof item === 'function' || item !== Object(item)) {
    return item;
  }
  return Object.fromEntries(
    Object.entries(item as Record<string, unknown>).map(
      ([key, value]: [string, unknown]) => [
        key.replace(/([-_][a-z])/gi, c => c.toUpperCase().replace(/[-_]/g, '')),
        recursiveToCamel(value),
      ],
    ),
  );
};
Halden answered 4/4, 2023 at 9:34 Comment(0)
G
6

For all the people using lodash I have manipulated the accepted answer since lodash already includes the utility functions like isArray isObject camelCase

So the code reduces to this. :)

function keysToCamel(obj) {
  if (isPlainObject(obj)) {
    const n = {};
    Object.keys(obj).forEach(k => (n[camelCase(k)] = keysToCamel(obj[k])));
    return n;
  } else if (isArray(obj)) obj.map(i => keysToCamel(i));
  return obj;
}
Glove answered 16/1, 2020 at 12:51 Comment(3)
This does not work because isObject in lodash returns true for arrays Checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0), and new String(''))Auburn
Typescript version: import { isPlainObject, camelCase, isArray } from "lodash"; function convertKeysToCamelCase(obj: unknown): unknown { if (isPlainObject(obj)) { const n = {}; Object.keys(obj as object).forEach( (k) => (n[camelCase(k)] = convertKeysToCamelCase((obj as object)[k])) ); return n; } else if (isArray(obj)) obj.map((i) => convertKeysToCamelCase(i)); return obj; } Mercator
In the if else statement, we should return the mapped array for it to work with Arrays of objects, Otherwises, objects inside arrays won't be transformedReveille
L
3

For those who are using typescript. I added typings to the accepted answer on this question.

const toCamel = (str: string): string => {
  return str.replace(/([_-][a-z])/gi, ($1: string) => {
    return $1.toUpperCase().replace('-', '').replace('_', '');
  });
};

const isArray = function (
  input: Record<string, unknown> | Record<string, unknown>[] | unknown
): input is Record<string, unknown>[] {
  return Array.isArray(input);
};

const isObject = function (
  obj: Record<string, unknown> | Record<string, unknown>[] | unknown
): obj is Record<string, unknown> {
  return (
    obj === Object(obj) && !Array.isArray(obj) && typeof obj !== 'function'
  );
};

const camelize = function <T>(input: T): T {
  return (function recurse<
    K extends Record<string, unknown> | Record<string, unknown>[] | unknown
  >(input: K): K {
    if (isObject(input)) {
      return Object.keys(input).reduce((acc, key) => {
        return Object.assign(acc, { [toCamel(key)]: recurse(input[key]) });
      }, {} as K);
    } else if (isArray(input)) {
      return input.map((i) => recurse(i)) as K;
    }
    return input;
  })(input);
};
Leta answered 26/4, 2021 at 11:59 Comment(0)
H
2

You can do this in a fairly generic fashion, writing a function which accepts an arbitrary key-transformation function and returning one which accepts an object and returns one with the same structure but with keys transformed. This is essentially no harder than writing code specifically for camelizing the keys.

Here is one approach:

const fixKeys = (fn) => (obj) => Object .fromEntries (
  Object .entries (obj) .map (([k, v]) => [
    fn(k), 
    Array .isArray (v) ? v .map (fixKeys (fn)) : Object (v) === v ? fixKeys (fn) (v) : v
  ])
)

const camelCase = (s) => s.replace(/_(.)/g, (s, c) => c.toUpperCase())

const camelizeKeys = fixKeys (camelCase)

const obj = {vt_core_random: {user_details: {first_name: "xyz", last_name: "abc", groups: [{id: 1, group_type: "EXT"}, {id: 2, group_type: "INT"}], address_type: { city_name: "nashik", state: {code_name: "MH",  name: "Maharashtra"}}}}}

console .log (camelizeKeys (obj))
.as-console-wrapper {max-height: 100% !important; top: 0}

If Object.fromEntries is not available in your environment it's easy enough to shim or replace. Also, this is tagged "lodash", and if you're already using it, you might want to replace some of the custom functionality here with lodash function, including the camelCase function and the Array and Object tests. Or not, as this is already fairly simple.

Handley answered 16/1, 2020 at 15:46 Comment(2)
there is a little problem if the value of a property is null, because null type is 'object'. from leonheess answer we could invert the object check to be: v !== Object(v) ? v : fixKeys (fn)(v)Rabbitry
@Marcelo: You're right. I've taken to using that in the last few years in place of typeof. I updated the answer to include it.Handley
S
1
const obj = {
  vt_core_random: {
    user_details: {
      first_name: "xyz",
      last_name: "abc",
      groups: [
        {
          id: 1,
          group_type: "EXT"
        },
        {
          id: 2,
          group_type: "INT"
        }
      ],
      address_type: {
        city_name: "nashik",
        state: {
          code_name: "MH",
          name: "Maharashtra"
        }
      }
    }
  }
};

function toCamel(o) {
  var newO, origKey, newKey, value
  if (o instanceof Array) {
    return o.map(function(value) {
        if (typeof value === "object") {
          value = toCamel(value)
        }
        return value
    })
  } else {
    newO = {}
    for (origKey in o) {
      if (o.hasOwnProperty(origKey)) {
        newKey = _.camelCase(origKey)
        value = o[origKey]
        if (value instanceof Array || (value !== null && value.constructor === Object)) {
          value = toCamel(value)
        }
        newO[newKey] = value
      }
    }
  }
  return newO
}

console.log(toCamel(obj));    const obj = {
  vt_core_random: {
    user_details: {
      first_name: "xyz",
      last_name: "abc",
      groups: [
        {
          id: 1,
          group_type: "EXT"
        },
        {
          id: 2,
          group_type: "INT"
        }
      ],
      address_type: {
        city_name: "nashik",
        state: {
          code_name: "MH",
          name: "Maharashtra"
        }
      }
    }
  }
};

function toCamel(o) {
  var newO, origKey, newKey, value
  if (o instanceof Array) {
    return o.map(function(value) {
        if (typeof value === "object") {
          value = toCamel(value)
        }
        return value
    })
  } else {
    newO = {}
    for (origKey in o) {
      if (o.hasOwnProperty(origKey)) {
        newKey = _.camelCase(origKey)
        value = o[origKey]
        if (value instanceof Array || (value !== null && value.constructor === Object)) {
          value = toCamel(value)
        }
        newO[newKey] = value
      }
    }
  }
  return newO
}

console.log(toCamel(obj));

I have used lodash in this code.

Sholom answered 16/1, 2020 at 12:35 Comment(0)
S
1

I used the code from Donny Verduijn, but found that Date objects were getting converted to {}.

I also needed the ability to convert from snakeToCamel and camelToSnake so I added a formatter parameter.

export const snakeToCamel = (str: string): string =>
  str.replace(/([_-][a-z])/gi, ($1: string) => $1.toUpperCase().replace('-', '').replace('_', ''));

export const camelToSnake = (str: string): string =>
  str.replace(/([A-Z])/g, ($1: string) => `_${$1.toLowerCase()}`);

const isArray = function (
  input: Record<string, unknown> | Record<string, unknown>[] | unknown
): input is Record<string, unknown>[] {
  return Array.isArray(input);
};

export const isObject = function (
  obj: Record<string, unknown> | Record<string, unknown>[] | unknown
): obj is Record<string, unknown> {
  return (
    isValidDate(obj) === false &&
    obj === Object(obj) &&
    !Array.isArray(obj) &&
    typeof obj !== 'function'
  );
};

const isValidDate = (value: any) => value instanceof Date;

const modifyObjectKeys = function <T>(input: T, formatter: (word: string) => string): T {
  return (function recurse<K extends Record<string, unknown> | Record<string, unknown>[] | unknown>(
    input: K
  ): K {
    if (isObject(input)) {
      return Object.keys(input).reduce(
        (acc, key) => Object.assign(acc, { [formatter(key)]: recurse(input[key]) }),
        {} as K
      );
    } else if (isArray(input)) {
      return input.map((i) => recurse(i)) as K;
    }
    return input;
  })(input);
};

/**
 *
 * @param input Object to convert keys to camelCase
 * @returns Object with keys converted to camelCase
 */
export const camelize = function <T>(input: T): T {
  return modifyObjectKeys(input, snakeToCamel);
};

/**
 *
 * @param input Object to convert keys to snake_case
 * @returns Object with keys converted to snake_case
 */
export const snakeify = function <T>(input: T): T {
  return modifyObjectKeys(input, camelToSnake);
};

Silden answered 15/5, 2022 at 3:59 Comment(0)
T
0

Use npm json-case-handler which will allow you to do this in one line.

It can convert any nested objects

For your case, you can do this :

const jcc = require('json-case-convertor')
const camelCasedJson = jcc.camelCaseKeys(yourjsonData)
Tip answered 3/8, 2022 at 11:13 Comment(0)
T
0

FP approach solution with lodash/fp

import { camelCase, toPairs, fromPairs, map, compose, isArray, isObject } from 'lodash/fp';

const convertAllKeysToCamelCase = compose(
  fromPairs,
  map(([key, value]) => {
    if (isArray(value)) {
      return [camelCase(key), map(convertAllKeysToCamelCase, value)];
    }

    if (isObject(value)) {
      return [camelCase(key), convertAllKeysToCamelCase(value)];
    }

    return [camelCase(key), value];
  }),
  toPairs
);

convertAllKeysToCamelCase(myObj)

Tiffany answered 28/9, 2022 at 12:22 Comment(0)
P
0

This question was recently updated, hence here is another solution using object-scan. Using a well documented library for the iteration might make it a bit easier to adjust your code should that ever be necessary.

.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/[email protected]/lib/index.min.js';

const obj = { vt_core_random: { user_details: { first_name: 'xyz', last_name: 'abc', groups: [{ id: 1, group_type: 'EXT' }, { id: 2, group_type: 'INT' }], address_type: { city_name: 'nashik', state: { code_name: 'MH', name: 'Maharashtra' } } } } };

const toCamel = (str) => str.replace(
  /([_-][a-z])/gi,
  (char) => char.toUpperCase().replace('-', '').replace('_', '')
);

objectScan(['**'], {
  filterFn: ({ parent, property, value }) => {
    if (typeof property === 'string') {
      const camelized = toCamel(property);
      if (camelized !== property) {
        // eslint-disable-next-line no-param-reassign
        parent[camelized] = value;
        // eslint-disable-next-line no-param-reassign
        delete parent[property];
      }
    }
  }
})(obj);

console.log(JSON.stringify(obj, null, 2));
/* => {
  "vtCoreRandom": {
    "userDetails": {
      "groups": [
        {
          "id": 1,
          "groupType": "EXT"
        },
        {
          "id": 2,
          "groupType": "INT"
        }
      ],
      "addressType": {
        "state": {
          "name": "Maharashtra",
          "codeName": "MH"
        },
        "cityName": "nashik"
      },
      "lastName": "abc",
      "firstName": "xyz"
    }
  }
} */
</script>

Disclaimer: I'm the author of object-scan

Proliferation answered 4/11, 2023 at 4:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.