{"id":13400185,"url":"https://github.com/solnic/virtus","last_synced_at":"2025-05-13T00:08:56.052Z","repository":{"id":427679,"uuid":"1559931","full_name":"solnic/virtus","owner":"solnic","description":"[DISCONTINUED ] Attributes on Steroids for Plain Old Ruby Objects","archived":false,"fork":false,"pushed_at":"2021-08-10T13:39:34.000Z","size":1853,"stargazers_count":3764,"open_issues_count":71,"forks_count":230,"subscribers_count":63,"default_branch":"master","last_synced_at":"2025-05-01T06:36:35.378Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/solnic.png","metadata":{"files":{"readme":"README.md","changelog":"Changelog.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2011-04-02T16:23:50.000Z","updated_at":"2025-04-19T03:59:46.000Z","dependencies_parsed_at":"2022-07-14T12:50:53.555Z","dependency_job_id":null,"html_url":"https://github.com/solnic/virtus","commit_stats":null,"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solnic%2Fvirtus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solnic%2Fvirtus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solnic%2Fvirtus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solnic%2Fvirtus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/solnic","download_url":"https://codeload.github.com/solnic/virtus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252466291,"owners_count":21752320,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T19:00:49.365Z","updated_at":"2025-05-13T00:08:56.013Z","avatar_url":"https://github.com/solnic.png","language":"Ruby","funding_links":[],"categories":["Ruby","Core Extensions"],"sub_categories":[],"readme":"[gem]: https://rubygems.org/gems/virtus\n[travis]: https://travis-ci.org/solnic/virtus\n[codeclimate]: https://codeclimate.com/github/solnic/virtus\n[coveralls]: https://coveralls.io/r/solnic/virtus\n[inchpages]: http://inch-ci.org/github/solnic/virtus/\n\nDISCONTINUED\n------------\n\n\u003e Working on virtus taught me a lot about handling data in Ruby, which involves coercions, type safety and validation (amongst other things). Even though the project has been successful, and serving well for many people, I decided to build something better. As a result, [dry-types](https://github.com/dry-rb/dry-types), [dry-struct](https://github.com/dry-rb/dry-struct) and [dry-schema](https://github.com/dry-rb/dry-schema) were born. These projects should be considered as virtus' successors, with better separation of concerns and better features. If you're interested in a modern take on same problems that virtus tried to solve, please check out these projects!\n\u003e\n\u003e @solnic\n\nVirtus\n======\n\n[![Gem Version](https://badge.fury.io/rb/virtus.svg)][gem]\n[![Build Status](https://travis-ci.org/solnic/virtus.svg?branch=master)][travis]\n[![Code Climate](https://codeclimate.com/github/solnic/virtus/badges/gpa.svg)][codeclimate]\n[![Test Coverage](https://codeclimate.com/github/solnic/virtus/badges/coverage.svg)][codeclimate]\n[![Inline docs](http://inch-ci.org/github/solnic/virtus.svg?branch=master)][inchpages]\n\nVirtus allows you to define attributes on classes, modules or class instances with\noptional information about types, reader/writer method visibility and coercion\nbehavior. It supports a lot of coercions and advanced mapping of embedded objects\nand collections.\n\nYou can use it in many different contexts like:\n\n* Input parameter sanitization and coercion in web applications\n* Mapping JSON to domain objects\n* Encapsulating data-access in Value Objects\n* Domain model prototyping\n\nAnd probably more.\n\nInstallation\n------------\n\n``` terminal\n$ gem install virtus\n```\n\nor in your **Gemfile**\n\n``` ruby\ngem 'virtus'\n```\n\nExamples\n--------\n\n### Using Virtus with Classes\n\nYou can create classes extended with Virtus and define attributes:\n\n``` ruby\nclass User\n  include Virtus.model\n\n  attribute :name, String\n  attribute :age, Integer\n  attribute :birthday, DateTime\nend\n\nuser = User.new(:name =\u003e 'Piotr', :age =\u003e 31)\nuser.attributes # =\u003e { :name =\u003e \"Piotr\", :age =\u003e 31, :birthday =\u003e nil }\n\nuser.name # =\u003e \"Piotr\"\n\nuser.age = '31' # =\u003e 31\nuser.age.class # =\u003e Fixnum\n\nuser.birthday = 'November 18th, 1983' # =\u003e #\u003cDateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)\u003e\n\n# mass-assignment\nuser.attributes = { :name =\u003e 'Jane', :age =\u003e 21 }\nuser.name # =\u003e \"Jane\"\nuser.age  # =\u003e 21\n```\n\n### Cherry-picking extensions\n\n``` ruby\n# include attribute DSL + constructor + mass-assignment\nclass User\n  include Virtus.model\n\n  attribute :name, String\nend\n\nuser = User.new(:name =\u003e 'Piotr')\nuser.attributes = { :name =\u003e 'John' }\nuser.attributes\n# =\u003e {:name =\u003e 'John'}\n\n# include attribute DSL + constructor\nclass User\n  include Virtus.model(:mass_assignment =\u003e false)\n\n  attribute :name, String\nend\n\nUser.new(:name =\u003e 'Piotr')\n\n# include just the attribute DSL\nclass User\n  include Virtus.model(:constructor =\u003e false, :mass_assignment =\u003e false)\n\n  attribute :name, String\nend\n\nuser = User.new\nuser.name = 'Piotr'\n```\n\n### Using Virtus with Modules\n\nYou can create modules extended with Virtus and define attributes for later\ninclusion in your classes:\n\n```ruby\nmodule Name\n  include Virtus.module\n\n  attribute :name, String\nend\n\nmodule Age\n  include Virtus.module(:coerce =\u003e false)\n\n  attribute :age, Integer\nend\n\nclass User\n  include Name, Age\nend\n\nuser = User.new(:name =\u003e 'John', :age =\u003e 30)\n```\n\n### Dynamically Extending Instances\n\nIt's also possible to dynamically extend an object with Virtus:\n\n```ruby\nclass User\n  # nothing here\nend\n\nuser = User.new\nuser.extend(Virtus.model)\nuser.attribute :name, String\nuser.name = 'John'\nuser.name # =\u003e 'John'\n```\n\n### Default Values\n\n``` ruby\nclass Page\n  include Virtus.model\n\n  attribute :title, String\n\n  # default from a singleton value (integer in this case)\n  attribute :views, Integer, :default =\u003e 0\n\n  # default from a singleton value (boolean in this case)\n  attribute :published, Boolean, :default =\u003e false\n\n  # default from a callable object (proc in this case)\n  attribute :slug, String, :default =\u003e lambda { |page, attribute| page.title.downcase.gsub(' ', '-') }\n\n  # default from a method name as symbol\n  attribute :editor_title, String,  :default =\u003e :default_editor_title\n\n  def default_editor_title\n    published? ? title : \"UNPUBLISHED: #{title}\"\n  end\nend\n\npage = Page.new(:title =\u003e 'Virtus README')\npage.slug         # =\u003e 'virtus-readme'\npage.views        # =\u003e 0\npage.published    # =\u003e false\npage.editor_title # =\u003e \"UNPUBLISHED: Virtus README\"\n\npage.views = 10\npage.views                    # =\u003e 10\npage.reset_attribute(:views)  # =\u003e 0\npage.views                    # =\u003e 0\n```\n\n### Default values on dynamically extended instances\n\nThis requires you to set `:lazy` option because default values are set in the\nconstructor if it's set to false (which is the default setting):\n\n``` ruby\nUser = Class.new\nuser = User.new\nuser.extend(Virtus.model)\nuser.attribute :name, String, default: 'jane', lazy: true\nuser.name # =\u003e \"jane\"\n```\n\n### Embedded Value\n\n``` ruby\nclass City\n  include Virtus.model\n\n  attribute :name, String\nend\n\nclass Address\n  include Virtus.model\n\n  attribute :street,  String\n  attribute :zipcode, String\n  attribute :city,    City\nend\n\nclass User\n  include Virtus.model\n\n  attribute :name,    String\n  attribute :address, Address\nend\n\nuser = User.new(:address =\u003e {\n  :street =\u003e 'Street 1/2', :zipcode =\u003e '12345', :city =\u003e { :name =\u003e 'NYC' } })\n\nuser.address.street # =\u003e \"Street 1/2\"\nuser.address.city.name # =\u003e \"NYC\"\n```\n\n### Collection Member Coercions\n\n``` ruby\n# Support \"primitive\" classes\nclass Book\n  include Virtus.model\n\n  attribute :page_numbers, Array[Integer]\nend\n\nbook = Book.new(:page_numbers =\u003e %w[1 2 3])\nbook.page_numbers # =\u003e [1, 2, 3]\n\n# Support EmbeddedValues, too!\nclass Address\n  include Virtus.model\n\n  attribute :address,     String\n  attribute :locality,    String\n  attribute :region,      String\n  attribute :postal_code, String\nend\n\nclass PhoneNumber\n  include Virtus.model\n\n  attribute :number, String\nend\n\nclass User\n  include Virtus.model\n\n  attribute :phone_numbers, Array[PhoneNumber]\n  attribute :addresses,     Set[Address]\nend\n\nuser = User.new(\n  :phone_numbers =\u003e [\n    { :number =\u003e '212-555-1212' },\n    { :number =\u003e '919-444-3265' } ],\n  :addresses =\u003e [\n    { :address =\u003e '1234 Any St.', :locality =\u003e 'Anytown', :region =\u003e \"DC\", :postal_code =\u003e \"21234\" } ])\n\nuser.phone_numbers # =\u003e [#\u003cPhoneNumber:0x007fdb2d3bef88 @number=\"212-555-1212\"\u003e, #\u003cPhoneNumber:0x007fdb2d3beb00 @number=\"919-444-3265\"\u003e]\n\nuser.addresses # =\u003e #\u003cSet: {#\u003cAddress:0x007fdb2d3be448 @address=\"1234 Any St.\", @locality=\"Anytown\", @region=\"DC\", @postal_code=\"21234\"\u003e}\u003e\n```\n\n### Hash attributes coercion\n\n``` ruby\nclass Package\n  include Virtus.model\n\n  attribute :dimensions, Hash[Symbol =\u003e Float]\nend\n\npackage = Package.new(:dimensions =\u003e { 'width' =\u003e \"2.2\", :height =\u003e 2, \"length\" =\u003e 4.5 })\npackage.dimensions # =\u003e { :width =\u003e 2.2, :height =\u003e 2.0, :length =\u003e 4.5 }\n```\n\n### IMPORTANT note about Boolean type\n\nBe aware that some libraries may do a terrible thing and define a global Boolean\nconstant which breaks virtus' constant type lookup, if you see issues with the\nboolean type you can workaround it like that:\n\n``` ruby\nclass User\n  include Virtus.model\n\n  attribute :admin, Axiom::Types::Boolean\nend\n```\n\nThis will be improved in Virtus 2.0.\n\n### IMPORTANT note about member coercions\n\nVirtus performs coercions only when a value is being assigned. If you mutate the value later on using its own\ninterfaces then coercion won't be triggered.\n\nHere's an example:\n\n``` ruby\nclass Book\n  include Virtus.model\n\n  attribute :title, String\nend\n\nclass Library\n  include Virtus.model\n\n  attribute :books, Array[Book]\nend\n\nlibrary = Library.new\n\n# This will coerce Hash to a Book instance\nlibrary.books = [ { :title =\u003e 'Introduction to Virtus' } ]\n\n# This WILL NOT COERCE the value because you mutate the books array with Array#\u003c\u003c\nlibrary.books \u003c\u003c { :title =\u003e 'Another Introduction to Virtus' }\n```\n\nA suggested solution to this problem would be to introduce your own class instead of using Array and implement\nmutation methods that perform coercions. For example:\n\n``` ruby\nclass Book\n  include Virtus.model\n\n  attribute :title, String\nend\n\nclass BookCollection \u003c Array\n  def \u003c\u003c(book)\n   if book.kind_of?(Hash)\n    super(Book.new(book))\n   else\n     super\n   end\n  end\nend\n\nclass Library\n  include Virtus.model\n\n  attribute :books, BookCollection[Book]\nend\n\nlibrary = Library.new\nlibrary.books \u003c\u003c { :title =\u003e 'Another Introduction to Virtus' }\n```\n\n### Value Objects\n\n``` ruby\nclass GeoLocation\n  include Virtus.value_object\n\n  values do\n    attribute :latitude,  Float\n    attribute :longitude, Float\n  end\nend\n\nclass Venue\n  include Virtus.value_object\n\n  values do\n    attribute :name,     String\n    attribute :location, GeoLocation\n  end\nend\n\nvenue = Venue.new(\n  :name     =\u003e 'Pub',\n  :location =\u003e { :latitude =\u003e 37.160317, :longitude =\u003e -98.437500 })\n\nvenue.location.latitude # =\u003e 37.160317\nvenue.location.longitude # =\u003e -98.4375\n\n# Supports object's equality\n\nvenue_other = Venue.new(\n  :name     =\u003e 'Other Pub',\n  :location =\u003e { :latitude =\u003e 37.160317, :longitude =\u003e -98.437500 })\n\nvenue.location === venue_other.location # =\u003e true\n```\n\n### Custom Coercions\n\n``` ruby\nrequire 'json'\n\nclass Json \u003c Virtus::Attribute\n  def coerce(value)\n    value.is_a?(::Hash) ? value : JSON.parse(value)\n  end\nend\n\nclass User\n  include Virtus.model\n\n  attribute :info, Json, default: {}\nend\n\nuser = User.new\nuser.info = '{\"email\":\"john@domain.com\"}' # =\u003e {\"email\"=\u003e\"john@domain.com\"}\nuser.info.class # =\u003e Hash\n\n# With a custom attribute encapsulating coercion-specific configuration\nclass NoisyString \u003c Virtus::Attribute\n  def coerce(value)\n    value.to_s.upcase\n  end\nend\n\nclass User\n  include Virtus.model\n\n  attribute :scream, NoisyString\nend\n\nuser = User.new(:scream =\u003e 'hello world!')\nuser.scream # =\u003e \"HELLO WORLD!\"\n```\n\n### Private Attributes\n\n``` ruby\nclass User\n  include Virtus.model\n\n  attribute :unique_id, String, :writer =\u003e :private\n\n  def set_unique_id(id)\n    self.unique_id = id\n  end\nend\n\nuser = User.new(:unique_id =\u003e '1234-1234')\nuser.unique_id # =\u003e nil\n\nuser.unique_id = '1234-1234' # =\u003e NoMethodError: private method `unique_id='\n\nuser.set_unique_id('1234-1234')\nuser.unique_id # =\u003e '1234-1234'\n```\n\n### Overriding setters\n\n``` ruby\nclass User\n  include Virtus.model\n\n  attribute :name, String\n\n  def name=(new_name)\n    custom_name = nil\n    if new_name == \"Godzilla\"\n      custom_name = \"Can't tell\"\n    end\n    super custom_name || new_name\n  end\nend\n\nuser = User.new(name: \"Frank\")\nuser.name # =\u003e 'Frank'\n\nuser = User.new(name: \"Godzilla\")\nuser.name # =\u003e 'Can't tell'\n\n```\n\n## Strict Coercion Mode\n\nBy default Virtus returns the input value even when it couldn't coerce it to the expected type.\nIf you want to catch such cases in a noisy way you can use the strict mode in which\nVirtus raises an exception when it failed to coerce an input value.\n\n``` ruby\nclass User\n  include Virtus.model(:strict =\u003e true)\n\n  attribute :admin, Boolean\nend\n\n# this will raise an error\nUser.new :admin =\u003e \"can't really say if true or false\"\n```\n\n## Nullify Blank Strings Mode\n\nIf you want to replace empty Strings with `nil` values (since they can't be\ncoerced into the expected type), you can use the `:nullify_blank` option.\n\n``` ruby\nclass User\n  include Virtus.model(:nullify_blank =\u003e true)\n\n  attribute :birthday, Date\nend\n\nUser.new(:birthday =\u003e \"\").birthday # =\u003e nil\n```\n\n\n## Building modules with custom configuration\n\nYou can also build Virtus modules that contain their own configuration.\n\n```ruby\nYupNopeBooleans = Virtus.model { |mod|\n  mod.coerce = true\n  mod.coercer.config.string.boolean_map = { 'nope' =\u003e false, 'yup' =\u003e true }\n}\n\nclass User\n  include YupNopeBooleans\n\n  attribute :name, String\n  attribute :admin, Boolean\nend\n\n# Or just include the module straight away ...\nclass User\n  include Virtus.model(:coerce =\u003e false)\n\n  attribute :name, String\n  attribute :admin, Boolean\nend\n```\n\n## Attribute Finalization and Circular Dependencies\n\nIf a type references another type which happens to not be available yet you need\nto use lazy-finalization of attributes and finalize virtus manually after all\ntypes have been already loaded:\n\n``` ruby\n# in blog.rb\nclass Blog\n  include Virtus.model(:finalize =\u003e false)\n\n  attribute :posts, Array['Post']\nend\n\n# in post.rb\nclass Post\n  include Virtus.model(:finalize =\u003e false)\n\n  attribute :blog, 'Blog'\nend\n\n# after loading both files just do:\nVirtus.finalize\n\n# constants will be resolved:\nBlog.attribute_set[:posts].member_type.primitive # =\u003e Post\nPost.attribute_set[:blog].type.primitive # =\u003e Blog\n```\n\n## Plugins / Extensions\n\nList of plugins/extensions that add features to Virtus:\n\n* [virtus-localized](https://github.com/XescuGC/virtus-localized): Localize the attributes\n* [virtus-relations](https://github.com/smanolloff/virtus-relations): Add relations to Virtus objects\n\nRuby version support\n--------------------\n\nVirtus is known to work correctly with the following rubies:\n\n* 1.9.3\n* 2.0.0\n* 2.1.2\n* jruby\n* (probably) rbx\n\nCredits\n-------\n\n* Dan Kubb ([dkubb](https://github.com/dkubb))\n* Chris Corbyn ([d11wtq](https://github.com/d11wtq))\n* Emmanuel Gomez ([emmanuel](https://github.com/emmanuel))\n* Fabio Rehm ([fgrehm](https://github.com/fgrehm))\n* Ryan Closner ([rclosner](https://github.com/rclosner))\n* Markus Schirp ([mbj](https://github.com/mbj))\n* Yves Senn ([senny](https://github.com/senny))\n\nContributing\n-------------\n\n* Fork the project.\n* Make your feature addition or bug fix.\n* Add tests for it. This is important so I don't break it in a\n  future version unintentionally.\n* Commit, do not mess with Rakefile or version\n  (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)\n* Send me a pull request. Bonus points for topic branches.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolnic%2Fvirtus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsolnic%2Fvirtus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolnic%2Fvirtus/lists"}