Skip to content
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

Enhance PORO documentation #1910

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 51 additions & 4 deletions docs/howto/serialize_poro.md
Expand Up @@ -2,13 +2,16 @@

# How to serialize a Plain-Old Ruby Object (PORO)

When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable, but pretty much any object can be serializable with ActiveModelSerializers. Here is an example of a PORO that is serializable:
### Using Duck Typing
When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable, but pretty much any object can be serializable with ActiveModelSerializers.
Here is an example of a PORO that is serializable just by implementing the needed methods:

```ruby
# my_model.rb
class MyModel
alias :read_attribute_for_serialization :send
attr_accessor :id, :name, :level

def initialize(attributes)
@id = attributes[:id]
@name = attributes[:name]
Expand All @@ -21,12 +24,56 @@ class MyModel
end
```

Fortunately, ActiveModelSerializers provides a [`ActiveModelSerializers::Model`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb) which you can use in production code that will make your PORO a lot cleaner. The above code now becomes:
### Inheriting ActiveModelSerializers::Model
Fortunately, ActiveModelSerializers provides a [`ActiveModelSerializers::Model`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb) which you can use in production code that will make your PORO a lot cleaner.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's missing from these docs is that AMS::Model also serves as executable documentation of the serializable interface, which can be copied or referenced without actually using the class.

The above code now becomes:
```ruby
# my_model.rb
class MyModel < ActiveModelSerializers::Model
attr_accessor :id, :name, :level
end
```

The default serializer would be `MyModelSerializer`.
The default serializer would be `MyModelSerializer`.

### Serializing an instance
Sometimes you don't want to create yet another class just to serialize an object.
Well, athough it is recommended to have defined classes for what you serialize,
this is possible as well:

Given the following serializer:

```ruby
class Api::V1::UserSerializer
type :user #this is necessery, otherwise AMS will show your instance class name

attributes :id, :name
```

```ruby
class Api::V1::UsersController
def show
#this is just an example
an_instance = OpenStruct.new(id: 1, name: 'Just an example')

an_instance.singleton_class.send(:alias_method, :read_attribute_for_serialization, :send)

render jsonapi: an_instance, serializer: Api::V1::UserSerializer
end
end
```

If your object is just a hash you can do the following trick:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to strongly recommend not trying to serialize primitives like Hashes. AMS is intended to models. There's a lot of parts of the AMS contract that would be weird to have on a hash. https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/lint.rb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by saying "models" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @bf4

Copy link
Member

@bf4 bf4 Sep 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vasilakisfil by model I mean a class that represents some domain concern. A Hash, being a primitive, doesn't do that. I think there's is a layer of AMS should be doing hash transformations, but that's not it's primary input, and that layer isn't yet exposed.

The imaginary pipeline is something like:

Given a model
Define attributes and associations to serializer (via ActiveModel::Serializer)
Serialize model to 'serializable_hash' per serializer and fields/include options (coordinated by adapter)

# This is where a hash would begin its journey, if this interface were exposed

Given a serializable hash
Transform in adapter

and then we would have a caching layer available in the pipeline

```ruby
class Api::V1::UsersController
def show
#this is just an example
an_instance = {id: 1, name: 'Just an example'}
an_instance.singleton_class.send(:alias_method, :read_attribute_for_serialization, :[])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe what we want here is something like:

Given a user instance, the serializer should act the user to get the defined attributes and associations. However, since the transformation for a user with id: 1 and name: 'Mortimer' could be compared to something like user_attributes = user.attributes; user_attributes.slice(:id, :name), wouldn't it be nice if we could start from user_attributesas{ id: 1, name: 'Mortimer' }` and have the serializer treat that hash as user_attributes?


render jsonapi: an_instance, serializer: Api::V1::UserSerializer,
end
end
```
Note however that if your hash keys are strings then you will need to convert them
to symbols first (or just wrap it in `HashWithIndifferentAccess`).