Repository: serverless/serverless (JavaScript)
Date: 2017-05-18 19:35:38+00:00
Task type: Feature

Issue:
Support for Serverless.JSON 
# This is a Feature Proposal
JSON support as Serverless config file (Serverless.json instead of Serverless.YML)
## Description

* Using YML feels awkard , most of the usage of lambda are done with NodeJS and Node mostly uses JSON file for configuration (Webpack, Grunt , package.json etc....)
Using YML doesn't feel right
* Most examples provided by AWS for cloudformation are using JSON ( examples here 
[AWS Labs](https://github.com/awslabs)

Similar or dependent issues:

This feature was already proposed but got lost in the different release [Optionnal Serverless.JSON](https://github.com/serverless/serverless/issues/1900)






Issue discussion:
AWS currently changes its CF documentation to contain both JSON and YAML snippets, e.g. http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html

YAML has a lot of advantages over JSON, e.g. using comments, assigning multiline strings to a property in a readable way.

According to the YAML specification (see http://yaml.org/spec/1.2/spec.html#id2759572):

> YAML can therefore be viewed as a natural superset of JSON, offering improved human readability and a more complete information model. This is also the case in practice; every JSON file is also a valid YAML file.

So, imo, there is no point of explicitly supporting JSON. If the parser is compliant it should be possible to write the serverless.yml as plain JSON and everything should continue to work.
> Using YML feels awkward

I felt this too when starting to use serverless. A solid IDE with yaml support / indentation support does wonders here. I recommend atom for all your yaml needs =)

Ultimately though, I agree with @HyperBrain. YAML is much more flexible than JSON (even if it is harder to parse / write to with all those comments)

@kuashe if you want to use JSON, you can do so with something like https://www.npmjs.com/package/json2yaml and compile that down to YAML before running `sls deploy`

here is an example of how that would work in package.json

```json
{
  "scripts": {
      "predeploy": "json2yaml ./serverless.json > ./serverless.yml",
      "deploy": "serverless deploy",
   }
}
```

@kuashe thanks for the proposal 👍 

I agree with @HyperBrain and @DavidWells here.

YAML is really a pain sometimes du to its idiosyncrasies.
However it provides some very cool enhancements which are not supported in plain JSON.

We had some plans to support JSON in the Serverless core but abandoned it due to technical debt and lack of interest within the community.

A solution could be to provide this via a plugin (e.g. based on @DavidWells code above :top:). Where the JSON is parsed into YAML and vice versa. Shouldn't be too hard to pull off.
@pmuens @DavidWells  There is no need for a plugin or any scripts at all! As I wrote in my comment above, a JSON file is a valid YAML in any case (per definition).
You just have to write a JSON service definition and save it as `serverless.yml`. If the contents are valid JSON it will work out of the box. Here is a test I did with the JSON service definition:

Contents of the `serverless.yml`:
```
{
  "service": "sls-test-project",
  "provider": {
    "name": "aws",
    "runtime": "nodejs4.3",
    "stage": "dev",
    "region": "us-east-1",
    "memorySize": 512,
    "timeout": 15,
    "vpc": {
      "securityGroupIds": [
        {
          "Fn::ImportValue": "${self:custom.networkStack}-PrivateSG"
        }
      ],
      "subnetIds": [
        {
          "Fn::ImportValue": "${self:custom.networkStack}-PrivateSubnet1"
        },
        {
          "Fn::ImportValue": "${self:custom.networkStack}-PrivateSubnet2"
        }
      ]
    },
    "iamRoleStatements": [
      {
        "Effect": "Allow",
        "Action": [
          "dynamodb:*"
        ],
        "Resource": [
          {
            "Fn::Join": [
              "/",
              [
                {
                  "Fn::Join": [
                    ":",
                    [
                      "arn:aws:dynamodb",
                      {
                        "Ref": "AWS::Region"
                      },
                      {
                        "Ref": "AWS::AccountId"
                      },
                      "table"
                    ]
                  ]
                },
                {
                  "Ref": "TestDynamoDbTable"
                }
              ]
            ]
          }
        ]
      }
    ],
    "stackTags": {
      "environment": "${opt:stage, self:provider.stage}",
      "application": "${self:service}",
      "product": "something"
    },
    "environment": {
      "SERVERLESS_PROJECT_NAME": "${self:service}",
      "SERVERLESS_PROJECT": "${self:service}",
      "SERVERLESS_STAGE": "${self:custom.stage}",
      "SERVERLESS_REGION": "${self:custom.region}",
      "TEST_TABLE_NAME": {
        "Ref": "TestDynamoDbTable"
      }
    }
  },
  "plugins": [
    "serverless-offline",
    "serverless-webpack",
    "serverless-plugin-warmup",
    "serverless-aws-alias"
  ],
  "custom": {
    "stage": "${opt:stage, self:provider.stage}",
    "region": "${opt:region, self:provider.region}",
    "networkStack": "network-${self:custom.stage}",
    "newService": "${self:}",
    "dev": {
      "testvar": "test-${self:custom.stage}"
    },
    "prod": {
      "testvar": "test-${self:custom.stage}"
    },
    "webpack": "./webpack.lambda.config.js",
    "webpackIncludeModules": true
  },
  "functions": {
    "testfct1": {
      "description": "Echo function echoes alias",
      "handler": "handlers/testfct1/handler.handle",
      "warmup": true,
      "events": [
        {
          "http": {
            "method": "GET",
            "path": "/func1"
          }
        }
      ]
    }
  },
  "resources": {
    "Resources": null,
    "Outputs": null
  }
}
```

This works as expected... It would be picky to just build a plugin for existing functionality just because I do not like the file extension 😕 

If wanted, the support of the JSON file extension could be added easily to the framework (thanks @pmuens for looking up the code location) here: https://github.com/serverless/serverless/blob/676d7283bad9aa646b5dc4c1b1e5f0427cb6e643/lib/classes/Service.js#L44-L49

As the parser fully supports JSON as is, it is sufficient to check for `serverless.json` in the second place (YAML first!).

Solution patch:
diff --git a/docs/providers/aws/guide/intro.md b/docs/providers/aws/guide/intro.md
index dfb190472a1..2bcb2833d76 100644
--- a/docs/providers/aws/guide/intro.md
+++ b/docs/providers/aws/guide/intro.md
@@ -58,7 +58,7 @@ The Serverless Framework not only deploys your Functions and the Events that tri
 
 ### Services
 
-A **Service** is the Framework's unit of organization.  You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`.  It looks like this:
+A **Service** is the Framework's unit of organization.  You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`).  It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/azure/guide/intro.md b/docs/providers/azure/guide/intro.md
index afd78a3b330..55fe71ca348 100644
--- a/docs/providers/azure/guide/intro.md
+++ b/docs/providers/azure/guide/intro.md
@@ -47,7 +47,7 @@ When you define an event for your Azure Function in the Serverless Framework, th
 
 ### Services
 
-A **Service** is the Framework's unit of organization.  You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`.  It looks like this:
+A **Service** is the Framework's unit of organization.  You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`).  It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/google/guide/intro.md b/docs/providers/google/guide/intro.md
index 321ed26ebd7..af49d99fb03 100644
--- a/docs/providers/google/guide/intro.md
+++ b/docs/providers/google/guide/intro.md
@@ -46,7 +46,7 @@ When you define an event for your Google Cloud Function in the Serverless Framew
 
 ### Services
 
-A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`. It looks like this:
+A **Service** is the Framework's unit of organization. You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`). It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/docs/providers/openwhisk/guide/intro.md b/docs/providers/openwhisk/guide/intro.md
index 26d848c8bb5..215093ac081 100644
--- a/docs/providers/openwhisk/guide/intro.md
+++ b/docs/providers/openwhisk/guide/intro.md
@@ -47,7 +47,7 @@ When you define an event for your Apache OpenWhisk Action in the Serverless Fram
 
 ### Services
 
-A **Service** is the Framework's unit of organization.  You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml`.  It looks like this:
+A **Service** is the Framework's unit of organization.  You can think of it as a project file, though you can have multiple services for a single application.  It's where you define your Functions, the Events that trigger them, and the Resources your Functions use, all in one file entitled `serverless.yml` (or `serverless.json`).  It looks like this:
 
 ```yml
 # serverless.yml
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 72026fad6f2..49707a6e601 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -33,7 +33,7 @@ class Service {
     const options = rawOptions || {};
     options.stage = options.stage || options.s;
     options.region = options.region || options.r;
-    const servicePath = that.serverless.config.servicePath;
+    const servicePath = this.serverless.config.servicePath;
 
     // skip if the service path is not found
     // because the user might be creating a new service
@@ -41,15 +41,29 @@ class Service {
       return BbPromise.resolve();
     }
 
-    let serverlessYmlPath = path.join(servicePath, 'serverless.yml');
-    // change to serverless.yaml if the file could not be found
-    if (!this.serverless.utils.fileExistsSync(serverlessYmlPath)) {
-      serverlessYmlPath = path
-        .join(this.serverless.config.servicePath, 'serverless.yaml');
-    }
+    // List of supported service filename variants.
+    // The order defines the precedence.
+    const serviceFilenames = [
+      'serverless.yaml',
+      'serverless.yml',
+      'serverless.json',
+    ];
+
+    const serviceFilePaths = _.map(serviceFilenames, filename => path.join(servicePath, filename));
+    const serviceFileIndex = _.findIndex(serviceFilePaths,
+      filename => this.serverless.utils.fileExistsSync(filename)
+    );
+
+    // Set the filename if found, otherwise set the preferred variant.
+    const serviceFilePath = serviceFileIndex !== -1 ?
+      serviceFilePaths[serviceFileIndex] :
+      _.first(serviceFilePaths);
+    const serviceFilename = serviceFileIndex !== -1 ?
+      serviceFilenames[serviceFileIndex] :
+      _.first(serviceFilenames);
 
     return that.serverless.yamlParser
-      .parse(serverlessYmlPath)
+      .parse(serviceFilePath)
       .then((serverlessFileParam) => {
         const serverlessFile = serverlessFileParam;
         // basic service level validation
@@ -58,18 +72,18 @@ class Service {
         if (ymlVersion && !semver.satisfies(version, ymlVersion)) {
           const errorMessage = [
             `The Serverless version (${version}) does not satisfy the`,
-            ` "frameworkVersion" (${ymlVersion}) in serverless.yml`,
+            ` "frameworkVersion" (${ymlVersion}) in ${serviceFilename}`,
           ].join('');
           throw new ServerlessError(errorMessage);
         }
         if (!serverlessFile.service) {
-          throw new ServerlessError('"service" property is missing in serverless.yml');
+          throw new ServerlessError(`"service" property is missing in ${serviceFilename}`);
         }
         if (_.isObject(serverlessFile.service) && !serverlessFile.service.name) {
-          throw new ServerlessError('"service" is missing the "name" property in serverless.yml');
+          throw new ServerlessError(`"service" is missing the "name" property in ${serviceFilename}`);   // eslint-disable-line max-len
         }
         if (!serverlessFile.provider) {
-          throw new ServerlessError('"provider" property is missing in serverless.yml');
+          throw new ServerlessError(`"provider" property is missing in ${serviceFilename}`);
         }
 
         if (typeof serverlessFile.provider !== 'object') {
@@ -84,7 +98,7 @@ class Service {
           const errorMessage = [
             `Provider "${serverlessFile.provider.name}" is not supported.`,
             ` Valid values for provider are: ${providers.join(', ')}.`,
-            ' Please provide one of those values to the "provider" property in serverless.yml.',
+            ` Please provide one of those values to the "provider" property in ${serviceFilename}`,
           ].join('');
           throw new ServerlessError(errorMessage);
         }
diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js
index 6790e646037..f5c6ac843d1 100644
--- a/lib/classes/Utils.js
+++ b/lib/classes/Utils.js
@@ -155,6 +155,8 @@ class Utils {
       servicePath = process.cwd();
     } else if (this.serverless.utils.fileExistsSync(path.join(process.cwd(), 'serverless.yaml'))) {
       servicePath = process.cwd();
+    } else if (this.serverless.utils.fileExistsSync(path.join(process.cwd(), 'serverless.json'))) {
+      servicePath = process.cwd();
     }
 
     return servicePath;
diff --git a/lib/plugins/package/lib/packageService.js b/lib/plugins/package/lib/packageService.js
index e1556f70f76..3609583644f 100644
--- a/lib/plugins/package/lib/packageService.js
+++ b/lib/plugins/package/lib/packageService.js
@@ -9,8 +9,9 @@ module.exports = {
     '.gitignore',
     '.DS_Store',
     'npm-debug.log',
-    'serverless.yaml',
     'serverless.yml',
+    'serverless.yaml',
+    'serverless.json',
     '.serverless/**',
   ],
 


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 49ab96f100a..a03d7dc885c 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -122,7 +122,7 @@ describe('Service', () => {
       return expect(noService.load()).to.eventually.resolve;
     });
 
-    it('should load from filesystem', () => {
+    it('should load serverless.yml from filesystem', () => {
       const SUtils = new Utils();
       const serverlessYml = {
         service: 'new-service',
@@ -175,6 +175,159 @@ describe('Service', () => {
       });
     });
 
+    it('should load serverless.yaml from filesystem', () => {
+      const SUtils = new Utils();
+      const serverlessYml = {
+        service: 'new-service',
+        provider: {
+          name: 'aws',
+          stage: 'dev',
+          region: 'us-east-1',
+          variableSyntax: '\\${{([\\s\\S]+?)}}',
+        },
+        plugins: ['testPlugin'],
+        functions: {
+          functionA: {},
+        },
+        resources: {
+          aws: {
+            resourcesProp: 'value',
+          },
+          azure: {},
+          google: {},
+        },
+        package: {
+          exclude: ['exclude-me'],
+          include: ['include-me'],
+          artifact: 'some/path/foo.zip',
+        },
+      };
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'),
+        YAML.dump(serverlessYml));
+
+      const serverless = new Serverless();
+      serverless.init();
+      serverless.config.update({ servicePath: tmpDirPath });
+      serviceInstance = new Service(serverless);
+
+      return expect(serviceInstance.load()).to.eventually.be.fulfilled
+      .then(() => {
+        expect(serviceInstance.service).to.be.equal('new-service');
+        expect(serviceInstance.provider.name).to.deep.equal('aws');
+        expect(serviceInstance.provider.variableSyntax).to.equal('\\${{([\\s\\S]+?)}}');
+        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
+        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
+        expect(serviceInstance.resources.azure).to.deep.equal({});
+        expect(serviceInstance.resources.google).to.deep.equal({});
+        expect(serviceInstance.package.exclude.length).to.equal(1);
+        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
+        expect(serviceInstance.package.include.length).to.equal(1);
+        expect(serviceInstance.package.include[0]).to.equal('include-me');
+        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
+      });
+    });
+
+    it('should load serverless.json from filesystem', () => {
+      const SUtils = new Utils();
+      const serverlessJSON = {
+        service: 'new-service',
+        provider: {
+          name: 'aws',
+          stage: 'dev',
+          region: 'us-east-1',
+          variableSyntax: '\\${{([\\s\\S]+?)}}',
+        },
+        plugins: ['testPlugin'],
+        functions: {
+          functionA: {},
+        },
+        resources: {
+          aws: {
+            resourcesProp: 'value',
+          },
+          azure: {},
+          google: {},
+        },
+        package: {
+          exclude: ['exclude-me'],
+          include: ['include-me'],
+          artifact: 'some/path/foo.zip',
+        },
+      };
+
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.json'),
+        JSON.stringify(serverlessJSON));
+
+      const serverless = new Serverless();
+      serverless.init();
+      serverless.config.update({ servicePath: tmpDirPath });
+      serviceInstance = new Service(serverless);
+
+      return expect(serviceInstance.load()).to.eventually.be.fulfilled
+      .then(() => {
+        expect(serviceInstance.service).to.be.equal('new-service');
+        expect(serviceInstance.provider.name).to.deep.equal('aws');
+        expect(serviceInstance.provider.variableSyntax).to.equal('\\${{([\\s\\S]+?)}}');
+        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
+        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
+        expect(serviceInstance.resources.azure).to.deep.equal({});
+        expect(serviceInstance.resources.google).to.deep.equal({});
+        expect(serviceInstance.package.exclude.length).to.equal(1);
+        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
+        expect(serviceInstance.package.include.length).to.equal(1);
+        expect(serviceInstance.package.include[0]).to.equal('include-me');
+        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
+      });
+    });
+
+    it('should load YAML in favor of JSON', () => {
+      const SUtils = new Utils();
+      const serverlessJSON = {
+        provider: {
+          name: 'aws',
+          stage: 'dev',
+          region: 'us-east-1',
+          variableSyntax: '\\${{([\\s\\S]+?)}}',
+        },
+        plugins: ['testPlugin'],
+        functions: {
+          functionA: {},
+        },
+        resources: {
+          aws: {
+            resourcesProp: 'value',
+          },
+          azure: {},
+          google: {},
+        },
+        package: {
+          exclude: ['exclude-me'],
+          include: ['include-me'],
+          artifact: 'some/path/foo.zip',
+        },
+      };
+
+      serverlessJSON.service = 'JSON service';
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.json'),
+        JSON.stringify(serverlessJSON));
+
+      serverlessJSON.service = 'YAML service';
+      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'),
+        YAML.dump(serverlessJSON));
+
+      const serverless = new Serverless();
+      serverless.init();
+      serverless.config.update({ servicePath: tmpDirPath });
+      serviceInstance = new Service(serverless);
+
+      return expect(serviceInstance.load()).to.eventually.be.fulfilled
+      .then(() => {
+        // YAML should have been loaded instead of JSON
+        expect(serviceInstance.service).to.be.equal('YAML service');
+      });
+    });
+
     it('should reject when the service name is missing', () => {
       const SUtils = new Utils();
       const serverlessYaml = {
@@ -189,7 +342,7 @@ describe('Service', () => {
       serviceInstance = new Service(serverless);
 
       return expect(serviceInstance.load()).to.eventually.be
-        .rejectedWith('"service" is missing the "name" property in serverless.yml');
+        .rejectedWith('"service" is missing the "name" property in');
     });
 
     it('should support service objects', () => {
diff --git a/lib/classes/Utils.test.js b/lib/classes/Utils.test.js
index 6dd47b1be7c..361fb01b1b8 100644
--- a/lib/classes/Utils.test.js
+++ b/lib/classes/Utils.test.js
@@ -272,6 +272,18 @@ describe('Utils', () => {
       expect(servicePath).to.not.equal(null);
     });
 
+    it('should detect if the CWD is a service directory when using Serverless .json files', () => {
+      const tmpDirPath = testUtils.getTmpDirPath();
+      const tmpFilePath = path.join(tmpDirPath, 'serverless.json');
+
+      serverless.utils.writeFileSync(tmpFilePath, 'foo');
+      process.chdir(tmpDirPath);
+
+      const servicePath = serverless.utils.findServicePath();
+
+      expect(servicePath).to.not.equal(null);
+    });
+
     it('should detect if the CWD is not a service directory', () => {
       // just use the root of the tmpdir because findServicePath will
       // also check parent directories (and may find matching tmp dirs
diff --git a/lib/plugins/package/lib/packageService.test.js b/lib/plugins/package/lib/packageService.test.js
index 8d76af4afa7..00c4a592f96 100644
--- a/lib/plugins/package/lib/packageService.test.js
+++ b/lib/plugins/package/lib/packageService.test.js
@@ -80,8 +80,8 @@ describe('#packageService()', () => {
       const exclude = packagePlugin.getExcludes();
       expect(exclude).to.deep.equal([
         '.git/**', '.gitignore', '.DS_Store',
-        'npm-debug.log',
-        'serverless.yaml', 'serverless.yml',
+        'npm-debug.log', 'serverless.yml',
+        'serverless.yaml', 'serverless.json',
         '.serverless/**', 'dir', 'file.js',
       ]);
     });
@@ -99,8 +99,8 @@ describe('#packageService()', () => {
       const exclude = packagePlugin.getExcludes(funcExcludes);
       expect(exclude).to.deep.equal([
         '.git/**', '.gitignore', '.DS_Store',
-        'npm-debug.log',
-        'serverless.yaml', 'serverless.yml',
+        'npm-debug.log', 'serverless.yml',
+        'serverless.yaml', 'serverless.json',
         '.serverless/**', 'dir', 'file.js',
         'lib', 'other.js',
       ]);