Create ERC721 contract with custom atributes (Not in the baseURI)

Good morning community,

I want to create a smart contract for a game where the ERC721 will mint NFTs which will represent planets. I have some properties as the type (fire/water...) and the rarity (common, uncommon...) that need to be on the NFT and not in the metadata (can not be modified once they are minted).

Following OpenZeppelin paradigm to create ERC721, I want to know what is the safest/proper way to add those 2 parameters in the smart contratact (i.e. safeMint("Common","Water"))

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract MyToken is ERC721, ERC721Burnable, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("MyToken", "MTK") {}

    function _baseURI() internal pure override returns (string memory) {
        return "";

    function safeMint(address to) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _safeMint(to, tokenId);

Thank you very much.

A good option may be to generate the metadata on chain.

You would have a function that based on your custom attributes concatenates strings to generate a JSON document, and then base64-encodes this document (you can use our Base64 library coming in v4.5), which can then be returned as a data URI in your tokenURI function.