In my Rails 7 app I'm receiving request from the external API. I want to check if the incoming request is valid. To do so I've to use strong parameters, here is the sample JSON request which hit my endpoint:
"sdd_request": {
"return_url": "https://example.com/return",
"data": {
"debit_method": "CORE",
"debtor": {
"name": "John Doe",
"email": "john.doe#example.com",
}
},
"extension": {
"signees": [{email: 'test#test.com', name: 'joe', last_name: 'smith' }],
"creditor": {
"id": "12345",
"name": "Acme Inc.",
"address": {
"city": "New York",
}
}
}
}
}
So the required are:
- return_url
- data
- signees
- creditor
How to require above parameters?
What I did was:
def sdd_setup_request_params
params.require(:sdd_request).permit(
:return_url,
data: [
:debit_method,
debtor: [
:name,
:email,
]
],
extension: [
signees: [],
creditor: [
:id,
:name,
address: [
:city,
]
]
]
]
)
end
But from what I understand that required whole object of sdd_request not individual components because if e.g. signees are missing it will nor raise an error of missing parameters am I right?
The role of strong parameters is not to validate the data. It's just to whitelist the parameters to avoid mass assignment vulnerabilities.
require is used to bail early if the general structure of the parameters makes it pointless to continue processing the request. Like for example in your typical Rails controller:
params.require(:person)
.permit(:name, :age, :city)
There is no point in continuing to process the request if the key :person is missing so a ActionController::ParameterMissing exception is raised which Rails rescues and returns a 400 - Bad Request response.
This prevents the potential nil errors that could occur when you expect a hash and get nil instead.
Validating the presence of the actual attributes is the job of the model in Rails.
class Person
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name
attribute :age
attribute :city
validates :name, :age, :city, presence: true
end
You might be thinking now: But waaaah I don't have a model because I'm gettting it from an API and I'm not saving it. Models can still be really useful even without persistence since the represent the entities in your application in a normalized form.
If you still really want to avoid a model for whatever reason there are gems that provide model-less validation in the controller with a bit more grace then a web of if statements.
While you can use require on nested hash structures like JSONAPI.org this will just return a single key and will raise on the first missing key so it cannot be used to give meaningful feedback about whats wrong with the input.
params.require(:data)
.require(:attributes)
.permit(:name, :age, :city)
Related
I have the following class:
class ArticlesController < ApplicationController
def create
article = Article.new(article_params)
end
private
def article_params
params.permit(:name, :age, book: [])
end
end
and I have a field called book that contains a collection followed by a hash [{...}], inside the hash object it can contain any random attribute, for example:
book_1 =
[
{
"id": "a1",
"type": "Color",
"title": "Live life cicle",
"content": "image_intro.png"
},
]
book_2 =
[
{
"id": "a2",
"email": "example#gmail.com",
"domain": "http://ddd.com"
}
]
...
book_7
[
{
"id": "a23",
"width": "3px",
"heigth": "5px",
"exist": true
}
]
What I would like is that every time I save a book, it can go through article_params no matter what attributes it contains within the hash, if you could help me please I would be grateful.
ActionController::Parameters does not have a "wildcard" syntax to allow any nested hash keys. But it does have #permit! which is an acknowledgement that strong parameters is not the solution for every possible problem.
permit! completely circumvents whitelisting by setting the permitted attribute on the ActionController::Parameters instance.
It will also set the permitted attribute on any nested instances of ActionController::Parameters - ie nested hashes in the parameters.
This is a very sharp tool which should be used with care.
In this case you might want to just use it on the nested attributes:
params.permit(:name, :age).merge(
books: params.dup.permit!.fetch(:books, [])
)
i have an issue with mapping third-party response to my server
the response is
"id": ".....",
"external_id": "....",
"recurring_payment_id": "....",
"is_high": ...,
"payment_method": "...",
and i set my strong params like
def invoice_params
params
.require(:invoice)
.permit({id: :invoice_id}, :external_id, :recurring_payment_id,
:payment_method)
end
i want to rename id to invoice_id
but i got an error
Unpermitted parameter: :id
I believe strong parameter does not support what you did. You need to work around it by rename the params key.
There is an easier method using alias, but use with caution. Add this to your class.
alias_attribute :id, :invoice_id
Edit: swap order because invoice_id is attribute in db
params[:id] = params.delete(:invoice_id)
params.permit(:id)
or
params[:invoice][:id] = params[:invoice].delete(:invoice_id)
params.require(:invoice).permit(:id)
I've been able to set up Active Storage file uploads and now I'm trying to return associated images when I do, for instance, Speaker.all or Speaker.find(2).
Calling the associated endpoint I get something like:
{
"id": 2,
"name": "Rafael",
"email": "rafael.almeida#mail-provider.com",
"company": "XING",
"social_media": "{\"twitter\": \"#rafaelcpalmeida\"}",
"created_at": "2018-10-01T17:21:50.993Z",
"updated_at": "2018-10-01T17:21:51.144Z"
}
How can I also return its associated avatar?
I figured out what to do in order to achieve the result I wanted. First, we need to add the active_model_serializers to the Gemfile, followed by bundle install.
After we installed the gem we should add include ActionController::Serialization to every controller that's going to use the Serializer.
We generate a new serializer using rails g serializer speaker. My SpeakerSerializer looks like:
class SpeakerSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :company, :avatar
def avatar
rails_blob_path(object.avatar, only_path: true) if object.avatar.attached?
end
end
And my output looks like
{
"speaker": {
"id": 2,
"name": "Rafael",
"email": "rafael.almeida#xing.com",
"company": "XING",
"avatar": "/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--515a0de8817b3529b5d3d168871cebf6ccee0463/xing-photo.jpg"
}
}
Check if this approach is good for your needs. This is the case of has_one_attached.
First, fetch the record:
speaker = Speaker.find(2)
Then convert it to a Ruby hash (please note .as_json):
speaker_hash = speaker.as_json
Now, just append the pair key-value you need, using url_for helper:
speaker_hash['url'] = url_for(speaker.avatar)
Finally, convert the hash to json:
speaker_hash.to_json
I have a Rails 5 app in which I use the gem active_model_serializers(https://github.com/rails-api/active_model_serializers). In my app I have a simplified data model that looks something like this:
# LocalizedString.rb
has_many :translations
# Translation.rb
belongs_to :localized_string
I'm trying to follow the best practices from JSON API, I have configured active_model_serializers like this:
ActiveModelSerializers.config.adapter = :json_api
When a user of the API requests translations (http://[root]/api/apps/117/translations) I currently get the following result:
{
"data": [
{
"id": "152",
"type": "translations",
"attributes": {
"value": "Test",
},
"relationships": {
"language": {
"data": {
"id": "1",
"type": "languages"
}
},
"localized-string": {
"data": {
"id": "162",
"type": "localized-strings"
}
}
}
},
[...]
From my localised-string I also want to include another attribute that is critical for the consumer of the API, and I don't want to have to make another API call to get the value of the attribute. I wonder what is the best / recommended way to do this that also follows json_api if possible.
Something like this could work:
"localized-string": {
"data": {
"id": "162",
"key": "my key value", # the attribute I need.
"type": "localized-strings"
}
}
But I'm not sure how to achieve that using active_model_serializers or if it is another recommended way of doing what I want with [json_api][1].
For completion, my relevant serialiser files looks lik this:
class TranslationSerializer < ActiveModel::Serializer
attributes :id, :value, :created_at, :updated_at
has_one :language
has_one :localized_string, serializer: LocalizedStringParentSerializer
end
class LocalizedStringParentSerializer < ActiveModel::Serializer
# I want to include the key attribute in my relationship object, but this doesn't work.
attributes :id, :key
end
So, any ideas on what I need to do to achieve what I want?
Per spec, relationships are represented by resource object identifiers. To include more than just the id and type, you'll want to use the include param. In AMS, I think that would be 'include: [:localizations], fields: { localizations: [:key]}' (not at computer now, but is approx right)
I'm trying to get the postgres_ext-serializers gem working, and I built a test project very similar to https://github.com/dockyard/postgres_ext-serializers/blob/master/test/test_helper.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :mobile
embed :ids, include: true
has_one :address, serializer: AddressSerializer
def include_mobile?
false
end
alias_method :include_address?, :include_mobile?
end
class AddressSerializer < ActiveModel::Serializer
attributes :id, :district_name
embed :ids, include: true
end
When I try to run the serializers the output doesn't seem to have nested elements. For example my serializer to_json output is:
"{\"users\":[{\"id\":1,\"name\":\"Aaron\",\"mobile\":null}, \n {\"id\":2,\"name\":\"Bob\",\"mobile\":null}],\"addresses\":[{\"id\":1,\"district_name\":\"Rob's Address\"}]}"
Notice how users and address are two separate elements of a hash, intead of being nested. If I remove the postgres_ext-serializers gem, then the output is as expected:
"[{\"id\":1,\"name\":\"Rob\",\"mobile\":null,\"address\":{\"id\":1,\"district_name\":\"Rob's Address\"}},{\"id\":2,\"name\":\"Bob\",\"mobile\":null,\"address\":null}]"
The address is embedded in the user hash exactly how I'm expecting it.
What am I missing, do I need to change anything to make the elements nested when using postgres_ext-serializers?
Thanks!
It seems, that the JSON you receive after serialization is what postgres_ext-serializers expects. Take a look into this test. expected_json in the first test case case is:
{
"users": [
{
"id": <UserID>,
"name": "John",
"mobile": "51111111",
"offer_ids": [],
"reviewed_offer_ids": []
}
],
"offers": [],
"addresses": [
{
"id": <AddressID>,
"district_name": "mumbai"
}
]
}
It looks very similar to the JSON you have received. But, to be honest, as include_address? method in your example returns false, I expect you must have not "addresses" field included into resulting JSON at all.