I tried several solutions here but none worked with multi-level data, such as if it includes a list of maps e.g.
{
  "item1": {
    "M": {
      "sub-item1": {
        "L": [
          {
            "M": {
              "sub-item1-list-map": {
                "S": "value"
Below, adapted from @igorzg's answer (which also has that drawback), fixes that.
Example usage:
dynamodb.getItem({...}, function(err, data) {
  if (!err && data && data.Item) {
    var converted = ddb_to_json(data.Item);
Here's the conversion function:
function ddb_to_json(data) {
  
  function isObject(value) {
    return typeof value === "object" && value !== null;
  }
  if(isObject(data))
    return convert_ddb({M:data});
  function convert_ddb(ddbData) {
    if (isObject(ddbData) && ddbData.hasOwnProperty('S'))
      return ddbData.S;
    if (isObject(ddbData) && ddbData.hasOwnProperty('N'))
      return parseFloat(ddbData.N);
    if (isObject(ddbData) && ddbData.hasOwnProperty('BOOL'))
      return ddbData.BOOL;
    if (isObject(ddbData) && ddbData.hasOwnProperty('NULL'))
      return null;
    if (isObject(ddbData) && ddbData.hasOwnProperty('M')) {
      var x = {};
      for(var k in ddbData.M)
        x[k] = convert_ddb(ddbData.M[k])
      return x;
    }
    if (isObject(ddbData) && ddbData.hasOwnProperty('L'))
      return ddbData.L.map(x => convert_ddb(x));
    if (isObject(ddbData) && ddbData.hasOwnProperty('SS'))
      return ddbData.SS;
    if (isObject(ddbData) && ddbData.hasOwnProperty('NN'))
      return ddbData.NN;
    if (isObject(ddbData) && ddbData.hasOwnProperty('BB'))
      return ddbData.BB;
    if (isObject(ddbData) && ddbData.hasOwnProperty('NS'))
      return ddbData.NS;
    if (isObject(ddbData) && ddbData.hasOwnProperty('BS'))
      return ddbData.BS;
    return data;
  }
  return data;
}