gitlab-org--gitlab-foss/app/serializers
Alexandru Croitor 0f6c42c5ce Move Multiple Issue Boards for Projects to Core
Refactor code to allow multiple issue boards management for projects
in CE
2019-06-26 12:28:00 +03:00
..
acts_as_taggable_on use lazy ajax filter dropdown for runner tags 2019-02-27 20:19:49 +01:00
concerns Fix slow performance with compiling HAML templates 2019-04-29 05:33:50 -07:00
error_tracking Add list_projects endpoint to error tracking 2019-02-06 16:27:18 +00:00
projects/serverless Add Knative metrics to Prometheus 2019-04-06 02:02:39 +00:00
analytics_build_entity.rb
analytics_build_serializer.rb
analytics_commit_entity.rb
analytics_commit_serializer.rb
analytics_generic_serializer.rb
analytics_issue_entity.rb
analytics_issue_serializer.rb
analytics_merge_request_entity.rb
analytics_merge_request_serializer.rb
analytics_stage_entity.rb Rephrase specs description for cycle analytics 2019-05-27 12:44:02 +02:00
analytics_stage_serializer.rb
analytics_summary_entity.rb
analytics_summary_serializer.rb
award_emoji_entity.rb
base_serializer.rb chore(rubocop): fix Style/TrivialAccessors issues 2019-01-16 13:53:04 +05:00
blob_entity.rb
board_serializer.rb Use serialization for project boards 2019-06-06 22:13:14 +12:00
board_simple_entity.rb Move Multiple Issue Boards for Projects to Core 2019-06-26 12:28:00 +03:00
build_action_entity.rb Add scheduled flag to job entity 2018-11-02 11:52:34 +00:00
build_artifact_entity.rb
build_details_entity.rb Extract Ci::Build#report_artifacts into method 2019-05-28 17:06:29 +02:00
build_metadata_entity.rb
build_serializer.rb
cluster_application_entity.rb Expose can_uninstall in cluster_status.json 2019-04-29 22:55:11 -07:00
cluster_entity.rb
cluster_serializer.rb
cohort_activity_month_entity.rb
cohort_entity.rb
cohorts_entity.rb
cohorts_serializer.rb
commit_entity.rb Add pipeline status to diffs/commit_item 2018-10-03 13:18:21 -05:00
container_repositories_serializer.rb
container_repository_entity.rb Add Container Registry API 2019-01-25 13:13:48 +01:00
container_tag_entity.rb Add Container Registry API 2019-01-25 13:13:48 +01:00
container_tags_serializer.rb
current_user_entity.rb Resolve "Filter discussion (tab) by comments or activity in issues and merge requests" 2018-10-23 09:49:45 +00:00
deploy_key_entity.rb
deploy_key_serializer.rb
deploy_keys_project_entity.rb
deployment_entity.rb Expose build environment latest deployable name and path 2019-05-24 10:04:35 +01:00
deployment_serializer.rb
detailed_status_entity.rb Run rubocop -a 2019-03-13 13:42:43 +00:00
diff_file_base_entity.rb Use Web IDE path for merge request edit buttons 2019-02-27 12:44:24 +00:00
diff_file_entity.rb Fixed expand full file button showing on images 2019-04-01 18:29:10 +01:00
diff_line_entity.rb Allow suggesting single line changes in diffs 2018-12-13 19:17:19 +00:00
diff_line_parallel_entity.rb
diff_line_serializer.rb
diff_viewer_entity.rb Replaced part of diff file properties with diff viewer 2019-02-15 17:56:50 +00:00
diffs_entity.rb Add pipeline status to diffs/commit_item 2018-10-03 13:18:21 -05:00
diffs_serializer.rb
discussion_diff_file_entity.rb Remove unused data from discussions endpoint 2018-12-06 17:07:49 -08:00
discussion_entity.rb Remove unused data from discussions endpoint 2018-12-06 17:07:49 -08:00
discussion_serializer.rb
entity_date_helper.rb Code style changes and refactor 2018-12-20 07:28:28 +08:00
entity_request.rb
environment_entity.rb Extract EE specific files/lines for Release app/serializers 2019-03-22 10:57:20 +07:00
environment_serializer.rb
environment_status_entity.rb Avoid returning deployment metrics url to MR widget when the deployment is not successful 2018-11-14 11:11:27 +00:00
environment_status_serializer.rb Move ci_environments_status to a model 2018-10-18 16:12:16 +02:00
group_child_entity.rb
group_child_serializer.rb
group_entity.rb
group_serializer.rb
group_variable_entity.rb Add control for variable value masking 2019-03-29 12:49:59 -06:00
group_variable_serializer.rb
issuable_entity.rb
issuable_sidebar_basic_entity.rb Code style changes and refactor 2018-12-20 07:28:28 +08:00
issuable_sidebar_extras_entity.rb [CE] Support multiple assignees for merge requests 2019-04-08 18:40:00 -03:00
issuable_sidebar_todo_entity.rb Refactor sidebar to use data from serializer 2018-12-19 17:58:53 +08:00
issue_board_entity.rb Add time tracking information to Issue Boards sidebar 2019-04-12 15:29:26 +05:30
issue_entity.rb Add doc links for confidential and locked issues 2019-06-06 10:37:02 +00:00
issue_serializer.rb Code style changes and refactor 2018-12-20 07:28:28 +08:00
issue_sidebar_basic_entity.rb Code style changes and refactor 2018-12-20 07:28:28 +08:00
issue_sidebar_extras_entity.rb [CE] Support multiple assignees for merge requests 2019-04-08 18:40:00 -03:00
job_artifact_report_entity.rb Rename with_all_reports to with_reports 2019-05-28 17:06:29 +02:00
job_entity.rb Allow to make builds soft-archived. 2018-11-05 15:51:57 +01:00
job_group_entity.rb
label_entity.rb Extract code into IssueBoardEntity 2018-12-04 08:44:21 -06:00
label_serializer.rb
lfs_file_lock_entity.rb
lfs_file_lock_serializer.rb
merge_request_assignee_entity.rb [CE] Support multiple assignees for merge requests 2019-04-08 18:40:00 -03:00
merge_request_basic_entity.rb [CE] Support multiple assignees for merge requests 2019-04-08 18:40:00 -03:00
merge_request_create_entity.rb
merge_request_create_serializer.rb
merge_request_diff_entity.rb Resolve "In Merge Request diff screen, master is not a hyperlink" 2019-01-16 13:18:10 +00:00
merge_request_for_pipeline_entity.rb Update pipeline detail view to accommodate post-merge pipelines 2019-03-19 23:19:27 +07:00
merge_request_metrics_entity.rb
merge_request_serializer.rb [CE] Support multiple assignees for merge requests 2019-04-08 18:40:00 -03:00
merge_request_sidebar_extras_entity.rb [CE] Support multiple assignees for merge requests 2019-04-08 18:40:00 -03:00
merge_request_user_entity.rb Resolve "Filter discussion (tab) by comments or activity in issues and merge requests" 2018-10-23 09:49:45 +00:00
merge_request_widget_commit_entity.rb Allow custom squash commit messages 2019-02-06 12:33:11 +00:00
merge_request_widget_entity.rb Omit issues links in merge request entity API response 2019-06-20 15:59:41 -07:00
move_to_project_entity.rb
move_to_project_serializer.rb
namespace_basic_entity.rb Improve the GitHub and Gitea import feature table interface 2019-02-13 00:15:57 +00:00
namespace_serializer.rb Improve the GitHub and Gitea import feature table interface 2019-02-13 00:15:57 +00:00
note_attachment_entity.rb
note_entity.rb Allow suggesting single line changes in diffs 2018-12-13 19:17:19 +00:00
note_user_entity.rb
pipeline_details_entity.rb Do not serialize a pipeline again when showing a build 2019-05-24 10:04:04 +01:00
pipeline_entity.rb Revert "Merge branch '57414-show-pipeline-iid-in-pipelines-page' into 'master'" 2019-06-14 13:17:18 +00:00
pipeline_serializer.rb Extract preloaded_relations into method 2019-04-09 13:24:27 +02:00
project_entity.rb
project_import_entity.rb Improve the GitHub and Gitea import feature table interface 2019-02-13 00:15:57 +00:00
project_mirror_entity.rb SSH public-key authentication for push mirroring 2018-11-19 11:46:39 +00:00
project_mirror_serializer.rb
project_note_entity.rb
project_note_serializer.rb
project_serializer.rb Improve the GitHub and Gitea import feature table interface 2019-02-13 00:15:57 +00:00
provider_repo_entity.rb Improve the GitHub and Gitea import feature table interface 2019-02-13 00:15:57 +00:00
provider_repo_serializer.rb Improve the GitHub and Gitea import feature table interface 2019-02-13 00:15:57 +00:00
README.md Extract code into IssueBoardEntity 2018-12-04 08:44:21 -06:00
remote_mirror_entity.rb SSH public-key authentication for push mirroring 2018-11-19 11:46:39 +00:00
request_aware_entity.rb
runner_entity.rb
stage_entity.rb
stage_serializer.rb
submodule_entity.rb
suggestion_entity.rb Fixed test specs 2019-04-04 13:08:34 +00:00
suggestion_serializer.rb Fixed test specs 2019-04-04 13:08:34 +00:00
test_case_entity.rb Display classname JUnit attribute in report modal 2019-05-20 13:27:08 +00:00
test_reports_comparer_entity.rb
test_reports_comparer_serializer.rb
test_suite_comparer_entity.rb
time_trackable_entity.rb
trigger_variable_entity.rb Add specs for TriggerVariableEntity 2018-12-07 17:07:58 +01:00
user_entity.rb
user_preference_entity.rb Add 'only history' option to notes filter 2018-11-05 12:30:14 +00:00
user_serializer.rb
variable_entity.rb Add control for variable value masking 2019-03-29 12:49:59 -06:00
variable_serializer.rb

Serializers

This is a documentation for classes located in app/serializers directory.

In GitLab, we use grape-entities, accompanied by a serializer, to convert a Ruby object to its JSON representation.

Serializers are typically used in controllers to build a JSON response that is usually consumed by a frontend code.

Why using a serializer is important?

Using serializers, instead of to_json method, has several benefits:

  • it helps to prevent exposure of a sensitive data stored in the database
  • it makes it easier to test what should and should not be exposed
  • it makes it easier to reuse serialization entities that are building blocks
  • it makes it easier to move complexity from controllers to easily testable classes
  • it encourages hiding complexity behind intentions-revealing interfaces
  • it makes it easier to take care about serialization performance concerns
  • it makes it easier to reduce merge conflicts between CE -> EE
  • it makes it easier to benefit from domain driven development techniques

What is a serializer?

A serializer is a class that encapsulates all business rules for building a JSON response using serialization entities.

It is designed to be testable and to support passing additional context from the controller.

What is a serialization entity?

Entities are lightweight structures that allow to represent domain models in a consistent and abstracted way, and reuse them as building blocks to create a payload.

Entities located in app/serializers are usually derived from a Grape::Entity class.

Serialization entities that do require to have a knowledge about specific elements of the request, need to mix RequestAwareEntity in.

A serialization entity usually maps a domain model class into its JSON representation. It rarely happens that a serialization entity exists without a corresponding domain model class. As an example, we have an Issue class and a corresponding IssueSerializer.

Serialization entites are designed to reuse other serialization entities, which is a convenient way to create a multi-level JSON representation of a piece of a domain model you want to serialize.

See documentation for Grape Entites for more details.

How to implement a serializer?

Base implementation

In order to effectively implement a serializer it is necessary to create a new class in app/serializers. See existing serializers as an example.

A new serializer should inherit from a BaseSerializer class. It is necessary to specify which serialization entity will be used to serialize a resource.

class MyResourceSerializer < BaseSerialize
  entity MyResourceEntity
end

The example above shows how a most simple serializer can look like.

Given that the entity MyResourceEntity exists, you can now use MyResourceSerializer in the controller by creating an instance of it, and calling MyResourceSerializer#represent(resource) method.

Note that a resource can be either a single object, an array of objects or an ActiveRecord::Relation object. A serialization entity should be smart enough to accurately represent each of these.

It should not be necessary to use Enumerable#map, and it should be avoided from the performance reasons.

Choosing what gets serialized

It often happens that you might want to use the same serializer in many places, but sometimes the intention is to only expose a small subset of object's attributes in one place, and a different subset in another.

BaseSerializer#represent(resource, opts = {}) method can take an additional hash argument, opts, that defines what is going to be serialized.

BaseSerializer will pass these options to a serialization entity. See how it is documented in the upstream project.

With this approach you can extend the serializer to respond to methods that will create a JSON response according to your needs.

class PipelineSerializer < BaseSerializer
  entity PipelineEntity

  def represent_details(resource)
    represent(resource, only: [:details])
  end

  def represent_status(resource)
    represent(resource, only: [:status])
  end
end

It is possible to use only and except keywords. Both keywords do support nested attributes, like except: [:id, { user: [:id] }].

Passing only and except to the represent method from a controller is possible, but it defies principles of encapsulation and testability, and it is better to avoid it, and to add a specific method to the serializer instead.

Reusing serialization entities from the API

Public API in GitLab is implemented using Grape.

Under the hood it also uses Grape::Entity classes. This means that it is possible to reuse these classes to implement internal serializers.

You can either use such entity directly:

class MyResourceSerializer < BaseSerializer
  entity API::Entities::SomeEntity
end

Or derive a new serialization entity class from it:

class MyEntity < API::Entities::SomeEntity
  include RequestAwareEntity

  unexpose :something
end

It might be a good idea to write specs for entities that do inherit from the API, because when API payloads are changed / extended, it is easy to forget about the impact on the internal API through a serializer that reuses API entities.

It is usually safe to do that, because API entities rarely break backward compatibility, but additional exposure may have a performance impact when API gets extended significantly. Write tests that check if only necessary data is exposed.

How to write tests for a serializer?

Like every other class in the project, creating a serializer warrants writing tests for it.

It is usually a good idea to test each public method in the serializer against a valid payload. BaseSerializer#represent returns a hash, so it is possible to use usual RSpec matchers like include.

Sometimes, when the payload is large, it makes sense to validate it entirely using match_response_schema matcher along with a new fixture that can be stored in spec/fixtures/api/schemas/. This matcher is using a json-schema gem, which is quite flexible, see a documentation for it.

How to use a serializer in a controller?

Once a new serializer is implemented, it is possible to use it in a controller.

Create an instance of the serializer and render the response.

def index
  format.json do
    render json: MyResourceSerializer
      .new(current_user: @current_user)
      .represent_details(@project.resources)
  end
end

If it is necessary to include additional information in the payload, it is possible to extend what is going to be rendered, the usual way:

def index
  format.json do
    render json: {
      resources: MyResourceSerializer
        .new(current_user: @current_user)
        .represent_details(@project.resources),
      count: @project.resources.count
    }
  end
end

Note that in these examples an additional context is being passed to the serializer (current_user: @current_user).

How to pass an additional context from the controller?

It is possible to pass an additional context from a controller to a serializer and each serialization entity that is used in the process.

Serialization entities that do require an additional context have RequestAwareEntity concern mixed in. This piece of the code exposes a method called request in every serialization entity that is instantiated during serialization.

An object returned by this method is an instance of EntityRequest, which behaves like an OpenStruct object, with the difference that it will raise an error if an unknown method is called.

In other words, in the previous example, request method will return an instance of EntityRequest that responds to current_user method. It will be available in every serialization entity instantiated by MyResourceSerializer.

EntityRequest is a workaround for #20045 and is meant to be refactored soon. Please avoid passing an additional context that is not required by a serialization entity.

At the moment, the context that is passed to entities most often is current_user and project.

Payload created by a serializer is usually a representation of the backed code, combined with the current request data. Therefore, technically, serializers are presenters that create payload consumed by a frontend code, usually Vue components.

In GitLab, it is possible to use presenters, but BaseSerializer still needs to learn how to use it, see #30898.

It is possible to use presenters when serializer is used to represent only a single object. It is not supported when ActiveRecord::Relation is being serialized.

MyObjectSerializer.new.represent(object.present)

Best practices

  1. Do not invoke a serializer from within a serialization entity.

    If you need to use a serializer from within a serialization entity, it is possible that you are missing a class for an important domain concept.

    Consider creating a new domain class and a corresponding serialization entity for it.

  2. Use only one approach to switch behavior of the serializer.

    It is possible to use a few approaches to switch a behavior of the serializer. Most common are using a Fluent Interface and creating a separate represent_something methods.

    Whatever you choose, it might be better to use only one approach at a time.

  3. Do not forget about creating specs for serialization entities.

    Writing tests for the serializer indeed does cover testing a behavior of serialization entities that the serializer instantiates. However it might be a good idea to write separate tests for entities as well, because these are meant to be reused in different serializers, and a serializer can change a behavior of a serialization entity.

  4. Use ActiveRecord::Relation where possible

    Using an ActiveRecord::Relation might help from the performance perspective.

  5. Be diligent about passing an additional context from the controller.

    Using EntityRequest and RequestAwareEntity is a workaround for the lack of high-level mechanism. It is meant to be refactored, and current implementation is error prone. Imagine the situation that one serialization entity requires request.user attribute, but the second one wants request.current_user. When it happens that these two entities are used in the same serialization request, you might need to pass both parameters to the serializer, which is obviously not a perfect situation.

    When in doubt, pass only current_user and project if these are required.

  6. Keep performance concerns in mind

    Using a serializer incorrectly can have significant impact on the performance.

    Because serializers are technically presenters, it is often necessary to calculate, for example, paths to various controller-actions. Since using URL helpers usually involve passing project and namespace adding includes(project: :namespace) in the serializer, can help to avoid N+1 queries.

    Also, try to avoid using Enumerable#map or other methods that will execute a database query eagerly.

  7. Avoid passing only and except from the controller.

  8. Write tests checking for N+1 queries.

  9. Write controller tests for actions / formats using serializers.

  10. Write tests that check if only necessary data is exposed.

  11. Write tests that check if no sensitive data is exposed.

Future