Skip to content

Allow for multiple naming conventions (camelCase vs kebab-case) #555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ahmadalli opened this issue Sep 22, 2019 · 9 comments
Closed

Allow for multiple naming conventions (camelCase vs kebab-case) #555

ahmadalli opened this issue Sep 22, 2019 · 9 comments
Assignees

Comments

@ahmadalli
Copy link

Description

json:api documentation recommends using camelCase instead of kebab case here and is discussed on json-api/json-api#1255.
Is it possible to change naming convention to camelCase or adding configuration for naming convention?

@wisepotato
Copy link
Contributor

This seems the way to go, with the option to turn on kebab-case if need be

@maurei
Copy link
Member

maurei commented Sep 23, 2019

You can define a newtonsoft ContractResolver on IJsonApiOptions, see this example.

Let me know if you figure it out

@maurei maurei closed this as completed Sep 23, 2019
@ahmadalli
Copy link
Author

I've changed the ContractResolver using options.SerializerSettings.ContractResolver = new CamelCaseContractResolver();

and this is my CamelCaseResolver:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Humanizer;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Serialization
{
    public class CamelCaseContractResolver : DefaultContractResolver
    {
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            JsonProperty property = base.CreateProperty(member, memberSerialization);

            property.PropertyName = property.PropertyName.Camelize();

            return property;
        }
    }
}

but I'm getting kebab-case result on my api

@ahmadalli
Copy link
Author

ahmadalli commented Sep 23, 2019

I've added this nameformatter:

using System;
using System.Reflection;
using Humanizer;
using JsonApiDotNetCore.Graph;
using JsonApiDotNetCore.Models;

namespace Serialization
{
    public class CamelCaseResourceNameFormatter : IResourceNameFormatter
    {
        public string FormatResourceName(Type resourceType)
        {
            try
            {
                // check the class definition first
                // [Resource("models"] public class Model : Identifiable { /* ... */ }
                if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute)
                    return attribute.ResourceName;

                return ApplyCasingConvention(resourceType.Name.Pluralize());
            }
            catch (InvalidOperationException e)
            {
                throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{resourceType}'.", e);
            }
        }

        public string FormatPropertyName(PropertyInfo property) => ApplyCasingConvention(property.Name);

        public string ApplyCasingConvention(string properName) => properName.Camelize();
    }
}

and I registered it on Startup.cs constructor: JsonApiOptions.ResourceNameFormatter = new CamelCaseResourceNameFormatter();

and now all of my resources are camelized except for the route names and pagination links are referring to wrong path

@maurei
Copy link
Member

maurei commented Sep 24, 2019

@ahmadalli

and now all of my resources are camelized except for the route names and pagination links are referring to wrong path

What exactly is the output you're having and what is it you're expecting? Do you mean they're still in kebab-case and you had expected the links to be pascalCase as well?

Good catch by the way, aIResourceNameFormatter implementation is indeed what you needed. And indeed, this class only affects

  • member names in the attributes and relationships objects in the api output (through the FormatPropertyName method)
  • the value of the "type" member (through FormatResourceName).

@ahmadalli
Copy link
Author

the routes remain kebab-case but the pagination links will use camelCase resource names which results in 404 error

@jaredcnance
Copy link
Contributor

The routing convention is applied during service registration here by the DasherizedRoutingConvention. At one point this was the accepted standard convention for json:api routing. As a quick hack you can disable the convention on a base class using [DisableRoutingConvention]:

var type = controller.ControllerType;
var notDisabled = type.GetCustomAttribute<DisableRoutingConventionAttribute>() == null;
return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin));

Otherwise, you can inject your own IApplicationModelConvention which will provide your own routing convention. This should unblock you. From a project standpoint, it would probably be good if the routing convention matched the resource naming conventions by default. This could be done by getting a name formatter here (the one challenge is you don't have access to the container in this scope):

var template = $"{_namespace}/{controller.ControllerName.Dasherize()}";

@maurei
Copy link
Member

maurei commented Sep 26, 2019

From a project standpoint, it would probably be good if the routing convention matched the resource naming conventions by default.

I agree. But I think we should generalise this further by adding the following rules:

  1. by default, the relationship names in the body and url (route or query parameters) should match too
  2. by default, the attribute names in the body and url (query parameters) should match too

So that would imply the following document with corresponding URLs:

GET /$TYPE/1?include=$RELATIONSHIP&field[$TYPE]=$ATTRIBUTE

{
  "data": {
    "type": "$TYPE",
    "id": "1",
    "attributes": {
      "$ATTRIBUTE": "JSON:API paints my bikeshed!"
    },
    "relationships": {
      "$RELATIONSHIP": {
          "data": null 
       },
       "links": {
          "self": "/$TYPE/1/relationships/$RELATIONSHIP",
        }
    }
  }
}

Additionally, to prevent inconsistent URLs like my-models/1/relationships/myRelationship, I would suggest to also add the rule
3. by default, apply the same casing convention to $ATTRIBUTE, $RELATIONSHIP and $TYPE.

Currently the official recommendation for $ATTRIBUTE and $RELATIONSHIP is camelCase. The default configuration with the rules outlined above would imply that the entire URL as well as the identifiers in the document body would be camelCase.

I would propose to have this as the default configuration, and on top of that allow for an option to use kebab-case everywhere instead of camelCase.

The specs recommendations for URLs are ambiguous in this matter, see json-api/json-api#1431 (comment)

Thoughts are welcome

@wisepotato wisepotato changed the title use camelCase naming convention instead of kebab-case Allow for multiple naming conventions (camelCase vs kebab-case) Oct 9, 2019
@maurei
Copy link
Member

maurei commented Oct 17, 2019

New insight. This is relevant: json-api/json-api#1091 (comment). For sparse field selection, if we stick to field[$TYPE]=$ATTRIBUTE as proposed in my previous comment, we would stuck with the constraint as described by the OP in the link above.

We currently already support sparse field selection on relationships and if we wish to keep on supporting this (which I think we should) we cannot go with $TYPE in the field selection because it would then be impossible to differentiate between articles?fields[author]=firstName and articles?fields[reviewer]=lastName (author and reviewer are both people).

Hence, updated proposal

GET /$TYPE/1?include=$RELATIONSHIP&fields=$ATTRIBUTE&fields[$RELATIONSHIP]=$ATTRIBUTE

{
  "data": {
    "type": "$TYPE",
    "id": "1",
    "attributes": {
      "$ATTRIBUTE": "JSON:API paints my bikeshed!"
    },
    "relationships": {
      "$RELATIONSHIP": {
          "data": null 
       },
       "links": {
          "self": "/$TYPE/1/relationships/$RELATIONSHIP",
        }
    }
  }
}

Note that this changes nothing with respect to normalizing the casing for $ATTRIBUTE, $RELATIONSHIP and $TYPE, because we would still want to avoid routes like my-models/1/relationships/myRelationship. About the latter: I'm making progress in configuring a IApplicationModelConvention with dependency injection, which would ultimately allow the developer to define a relationship between $ATTRIBUTE, $RELATIONSHIP and $TYPE as occurring in the URL and body as they please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

4 participants